Code Monkey home page Code Monkey logo

bones's Introduction

Bones

A 'meta-engine' framework made to facilitate the development of moddable, multiplayer 2D games.

Documentation Crates.io docs.rs Main Branch Docs License Discord


Initially borne out of Jumpy, Bones will eventually be the engine of choice for all Fish Folk games. It is suitable for any other games with similar requirements.

By default Bones is rendered with Bevy, but it is fundamentally engine-agnostic and comes with its own lightweight ECS, asset server and user experience. Bones is officially focused on 2D games and models itself after the likes of Corgi. It can however be used for 3D games as well, if you are willing to create custom rendering integrations.

Overview

Bones ECS

Bones is designed around a simple, custom Entity Component System (ECS), designed to make it easier to get a few features that are important to us:

  • Determinism: Bones ECS is deterministic by default, making it easier to get a re-producible and predictable gameplay.
  • Snapshot/Restore: The Bones ECS world can be trivially snapshot and restored.
  • Modding/Scripting: Bones ECS is built on our bones_schema system, which allows for runtime reflection and the ability to interact with data types defined outside of Rust.

Determinism and Snapshot/Restore are also key features for getting excellent network play with the rollback networking model, while requiring no changes to the core game loop implementation.

Bones Lib

The bones_lib contains the bones_ecs and the bones_asset system. It defines the concept of a Game which contains all of your game logic and data in a collection of Sessions that each have their own ECS World.

Bones lib has no rendering components or even math types, it is only concerned with organizing your game logic and assets.

Bones Framework

On top of bones_lib there is the bones_framework, which defines the rendering components and math types. Right now bones_framework is focused only on 2D rendering. 3D is not a priority for us now, but there is no technical limitation preventing community developed 3D rendering components either on top of bones_lib directly or as an extension to the bones_framework.

Bones Bevy Renderer

A game created with only the bones_framework is renderer agnostic, allowing us to create rendering integrations with other engines. Our official integration is with the [Bevy] engine.

Rendering in the bones_framework is intentionally simple, and some games may need more advanced features that aren't supported out of the box. Bones, and it's Bevy integration, are designed so that you can create custom rendering specific to your needs. That means you can still take advantage of any fancy new Bevy plugins, or maybe use something other than Bevy entirely!

Bones Scripting

bones_ecs is built to be scripted. Effort has also been made to avoid putting unnecessary performance limitations into the scripting system. Bones comes with an integration with the piccolo VM to enable Lua scripting out-of-the-box.

This integration allows Lua scripts to access the ECS world in a way very similar to the Rust API. Rust components and resources can be annotated with #[repr(C)] to enable direct access by Lua scripts, and if a type cannot be #[repr(C)], you can still manually create your own Lua bindings for that type.

Allowing both Rust and the scripting language to talk to the same ECS world allows you to easily blend both languages in your game, and have them interact quite easily in many circumstances. If a portion of your game needs extra high performance or low-level access, you can use Rust, but if you want hot reloaded and moddable elements of your game, you can use Lua.

The scripting system is not limited to Lua. Using the simple dynamic API to bones_ecs, you can create your own integrations to any language or system you desire.

The scripting system is new and work-in-progress, but all of the major things have been successfully implemented, and it is going to be actively used in Jumpy.

Contributing

If you would like to contribute, feel free to reach out on our Discord server to ask questions!

We also use TODO Issue to automatically create issues from all of our TODO comments in code. You can check out the todo issue list to see if there's any thing you'd like to take a hack at.

Similar Projects

Our architecure has many things in common with these other awesome projects:

bones's People

Contributors

bayou-brogrammer avatar dependabot[bot] avatar erlend-sh avatar maxcwhitehead avatar nelson137 avatar pieking1215 avatar piturnah avatar rbran avatar zicklag avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

bones's Issues

Allow Changing Sprite Color

We want you to be able to set the sprite "color" or "tint": the color the sprite pixels are multiplied by, which is pure white ( no change ) by default.

This should be a simple change.

We just need to add a color field to Sprite here:

pub struct Sprite {
/// The sprite image handle.
pub image: Handle<Image>,
/// Whether or not the flip the sprite horizontally.
pub flip_x: bool,
/// Whether or not the flip the sprite vertically.
pub flip_y: bool,
}

And we need to set the Bevy sprite color from the bones sprite color here:

sprite.flip_x = bones_sprite.flip_x;
sprite.flip_y = bones_sprite.flip_y;

investigate possible ways to avoid allocating vectors every frame for event lists.

mut keyboard_events: EventReader<KeyboardInput>,
) -> (bones::MouseInputs, bones::KeyboardInputs) {
// TODO: investigate possible ways to avoid allocating vectors every frame for event lists.
(
bones::MouseInputs {
movement: mouse_motion_events
.iter()
.last()


This issue was generated by todo-issue based on a TODO comment in 6164d35.

Double-Check `World.maintain()` is Working Properly

There's been at least one, and maybe two cases where we've run into a killed entity getting re-created ( with a different generation and the same entity index ), and then reading stale components from the deleted entity. ( See also #82 )

I just realized, though, that calling world.maintain() every loop should at least remove the component storage for killed entities, so that may not actually be removing stale component data properly. We need to test and make sure it's doing what it should be.

World Checksum

For troubleshooting determinism failures, it'd be good to have a way to checksum the world. We could do that by having an optional checksum function pointer in our untyped storage, similar to how we have an optional function pointer for cloning non-Copy types.

Allow deriving `hash_fn` and `eq_fn` for `HasSchema`.

default_fn: #default_fn,
drop_fn: Some(<Self as #schema_mod::raw_fns::RawDrop>::raw_drop),
// TODO: Allow deriving `hash_fn` and `eq_fn` for `HasSchema`.
eq_fn: None,
hash_fn: None,
kind: #schema_mod::SchemaKind::Primitive(#schema_mod::Primitive::Opaque {
size: layout.size(),
align: layout.align(),


This issue was generated by todo-issue based on a TODO comment in 6164d35.

Create custom system parameter to prevent needing to manualy `.borrow()` `AssetProvidersResource`.

/// The type of the [`AssetProviders`] resource.
// TODO: Create custom system parameter to prevent needing to manualy `.borrow()` `AssetProvidersResource`.
#[derive(Deref, DerefMut, Clone, TypeUlid)]
#[ulid = "01GNWY5HKV5JZQRKG20ANJXHCK"]


This issue was generated by todo-issue based on a TODO comment in 27a70cd. It's been assigned to @zicklag because they committed the code.

Evaluate possibility of avoiding an `Arc` clone.

We might be able to just just pass references with a lifetime for acessing it, instead of cloning the Arc like we do here.


pub layout: Layout,
/// Cell containing the raw pointer to the resource's data
// TODO: Evaluate possibility of avoiding an `Arc` clone.
// We might be able to just just pass references with a lifetime for acessing it,
// instead of cloning the Arc like we do here.
pub cell: Arc<AtomicRefCell<*mut u8>>,
/// A function that may be called to clone the resource from one pointer to another.
pub clone_fn: unsafe extern "C" fn(*const u8, *mut u8),


This issue was generated by todo-issue based on a TODO comment in 27a70cd. It's been assigned to @zicklag because they committed the code.

Double-Check Caching in Docs CI Job

I think something might be wrong with our cache for the docs job in CI. It seems to take longer than it should, but I haven't looked into it yet. I could be wrong.

Prevent Reading Dead Entities From Component Storage

Say you do this.

world.run_system(
  |mut entities: ResMut<Entities>, positions: CompMut<Pos>| 
    // Create an entity
    let entity = entities.create();
    positions.insert(entity, default());

    // Kill that entity
    entities.kill(entity);
    
    // This works
    let pos = positions.get(entity).unwrap();
  });

The issue is that the component storage doesn't know which entities are alive. So if you want this to behave as you would probably expect, you must do something like this:

world.run_system(
  |mut entities: ResMut<Entities>, positions: CompMut<Pos>| 
    // Create an entity
    let entity = entities.create();
    positions.insert(entity, default());

    // Kill that entity
    entities.kill(entity);
    
    // Pre-check entity live-ness
    if entities.is_alive(entity) {
      let pos = positions.get(entity).unwrap();
    }
  });

And that's not cool.


Maybe we can store store the list of valid generations for entities in the component stores, and update that every frame. So the example above would still work counter-intuitively, but after one frame the component store would become aware of the entity being deleted?

That's still kind of confusing.

I'm not sure if we can make some sort of atomic bitset that can efficiently update the list of alive entities across all components. We could put an Arc<AtomicRefCell<Bitset>> or something like that to keep track of alive entities, and then all component storages have a reference to the alive entities, maybe. That might hurt performance because of the locking, but it would only have to lock inefficiently when you have mutable access to the Entities in a system, so maybe that wouldn't be a problem.

I think that any solution that improves the ergonomics here, and doesn't seriously impact performance should probably be considered, even if it isn't perfect, or the fastest thing on the planet.

I haven't done a lot of in-depth thinking about different ways to design the ECS that might remedy this issue. Most of the overall design idea was borrowed from Planck, but there might be some divergence we can make here that might be more efficient and fix the confusion, I'm not sure.

Update To Slimmer Version of `bevy_simple_tilemap`

In order to get bones published to crates.io, we started depending on the released version of bevy_simple_tilemap which pulls in a lot of bevy features and dependencies that we don't need.

We have an open PR to address this: forbjok/bevy_simple_tilemap#9.

As soon as that merges and gets released we want to update so that we can strip out those dependencies again.

evaluate cost of initializing systems.

impl SessionRunner for DefaultSessionRunner {
fn step(&mut self, world: &mut World, stages: &mut SystemStages) -> SystemResult {
// TODO: evaluate cost of initializing systems.
//
// We need to find out how long it takes to initialize systems and decide whether or not it
// is worth finding a way to avoid doing it every step. The issue is finding a place to make
// sure the initialization happens for every system at least once.
stages.initialize_systems(world);


This issue was generated by todo-issue based on a TODO comment in 6164d35.

Re-evaluate `EcsError` variants.

Some of them may not be used anymore.


/// The types of errors used throughout the ECS.
// TODO: Re-evaluate `EcsError` variants.
// Some of them may not be used anymore.
#[derive(Debug, thiserror::Error)]
pub enum EcsError {
/// A resource was not initialized in the [`World`][crate::World] but the


This issue was generated by todo-issue based on a TODO comment in 27a70cd. It's been assigned to @zicklag because they committed the code.

Consider Removing the `TypeUlid` Requirement for Components and Resources

Right now bones makes heavy use of the TypeUlid trait to identify types uniquely apart from their unstable Rust TypeIds.

I'm realizing, though, that it might be more appropriate to take the approach used by Bevy for non-Rust types, which is to require all components and resources to be registered with the world, at which point they are given a ComponentId or a ResourceId.

Additionally, at registration, I think we would allow specifying a type path of sorts, that would look like a Rust module path probably.

When registering a type, you provide it's layout and its module path, and it would fail to register if you had already inserted a type with the same module path.

The module paths give us a much more explanatory way to reference the types in future scripting APIs, too, because otherwise we would be using the ULID, which has absolutely no correlation to the actual type being referenced.

Finally, this would make it much easier to use components in Rust, because we don't have to add a TypeUlid to everything.

Luckily, this change wouldn't really break any existing code. It would just remove the requirement to have a TypeUlid. We'd have to try it out in the codebase, though, just to make sure we're not using TypeUlids in some context where a pre-registered component/resource ID isn't going to work very well, but I think we'd be fine.

Attempt to refactor component store so we can avoid the two-step borrow process.

/// Get the components of a certain type
// TODO: Attempt to refactor component store so we can avoid the two-step borrow process.
//
// With resources we were able to avoid this step, but component stores are structured
// differently and might need more work.
pub fn get_cell<T: HasSchema>(&self) -> Result<AtomicComponentStore<T>, EcsError> {
let untyped = self.get_cell_by_schema_id(T::schema().id())?;


This issue was generated by todo-issue based on a TODO comment in 6164d35.

Alternative Entity Allocation Strategy

From chat:

It already removes dead entities from entities.iter_with(), and that's because it requires you to use the entities list to iterate, so it knows which ones are alive.

[17:57]
It just isn't automatic when using some_component.get() because it can't know which entities are alive without borrowing the entities.

[17:57]
Maybe we make some equivalent to some_component.get() that takes entities as an argument?

[17:57]
(edited)
Or maybe we do entities.get(some_component, entity) instead.

[17:58]
And then we recommend against using some_component.get() because it won't detect dead entities.

Yendor#2746
Yesterday at 18:14
how does bevy currently deal with this?

[18:14]
i think it might error out that entity doersnt exist

Zicklag
Yesterday at 18:14
Good question

[18:15]
I think the closest analogy to bones might be that all systems/components implicitly borrow Res<Entities> and therefore have access to the alive entities list, but that makes it impossible to borrow ResMut<Entities> in any system, and you are required to use Commands in order to create entities.

[18:16]
The tricky part might be how to allocate new entity IDs across multiple systems that might be running in parallel.

[18:17]
I don't think there's a way to do that deterministically.

[18:17]
Right now we don't do any multi-threading in bones, so maybe it's just not a problem.

[18:17]
(edited)
If we did multi-threading at all I think that might kill determinism gaurantees anyway.

Yendor#2746
Yesterday at 18:18
well thats the issue with bevy currently is that there is no determinism when running systems without explict system ordering

Zicklag
Yesterday at 18:18
Yeah, so if we say that we only guarantee determinism when not multi-threading...

[18:19]
Then we might be able to use an AtomicUsize to allocate new entities when you need to create them in a system.

[18:20]
Maybe we can just make the whole Entities resource completely thread safe.

[18:20]
So you can create and kill entities with only a Res<Entities>.

[18:21]
And then all of the Comp<SomComponent> borrows would have a handle to Entities that it could use to know when each entity is alive or not.

Yendor#2746
Yesterday at 18:30
Having the comp have access to entities is definitely a win there. I am a bit hesitant to hand the user the api to kill and create entities on demand

[18:32]
I think if we could hide that implementation and only use it within bones for components that would be ideal. Giving the user that much control over the system could lead to a foot gun scenario

Zicklag
Yesterday at 18:56
I am a bit hesitant to hand the user the api to kill and create entities on demand

[18:56]
I think that it's not that differet from the way we have it today.

[18:56]
We have to have a way to add and delete entities somehow.

[18:59]
We might be able to steal the entity allocator from [hecs](https://docs.rs/hecs), though we'd have to see if it was a good fit.

[18:59]
I think Bevy's is too baked into the grander ECS to be stealable by itself.

In short, the idea is to make Entites thread safe so that component storage can always know when an entity is alive and avoid returning dead entities.

This would fix #82.

Update `bevy_simple_tilemap` when our PR is merged.

forbjok/bevy_simple_tilemap#9


serde_json = "1.0"
serde_yaml = "0.9"
# TODO: Update `bevy_simple_tilemap` when our PR is merged.
# https://github.com/forbjok/bevy_simple_tilemap/pull/9
bevy_prototype_lyon = "0.8"
# Disable default features to remove rayon par-iter, is not performant. Once https://github.com/forbjok/bevy_simple_tilemap/pull/17 is merged,
# switch off fork back to original crate.


This issue was generated by todo-issue based on a TODO comment in 27a70cd. It's been assigned to @zicklag because they committed the code.

Replace ComponentStore functions with non-validating ones.

}
// TODO: Replace ComponentStore functions with non-validating ones.
//
// Right now functions like `insert`, `get`, and `get_mut` use the checked and panicing versions
// of the `untyped` functions. These functions do an extra check to see that the schema matches,
// but we've already validated that in the construction of the `ComponentStore`, so we should
// bypass the extra schema check for performance.


This issue was generated by todo-issue based on a TODO comment in 6164d35.

Error when using quinn_runtime_bevy with Bevy 0.9.1

Hello, I'm currently trying to use quinn_runtime_bevy with Bevy 0.9.1 but when I run my program it tells me thread 'IO Task Pool (0)' panicked at 'A IoTaskPool has not been initialized yet. Please call IoTaskPool::init beforehand.', [...] whereas when I don't use Bevy directly but bevy_tasks with version 0.8.1 it works fine.

I think the solution is to update bevy_tasks to the latest version for quinn_runtime_bevy.

System Parameters & Overlapping Borrows

This issue is to discuss an ergonomics issue that I've faced both in Bones ECS and in Bevy ECS, and to explore whether or not there's a good solution.

The scenario is simple:

I have a custom system parameter named CollisionWorld that contains helper methods and logic around detecting collisions and manipulating the physics world.

As a part of that CollisionWorld parameter, I borrow the Transform components mutably. This allows me to, as a part of collision world methods like translate move entities, and also to get the entities positions, as is necessary for collision detection.

This causes an ergonomics problem when any the user wants to include both a CollisionWorld system param, and a CompMut<Transforms> system param, because that is a conflict: you have two mutable borrows of Transform components.

The current solution in my WIP branch in jumpy is to make the transforms: CompMut<Transform> field of CollisionWorld public. This allows you to access a borrow of the transforms, but it's not a perfect solution, and users are not going to expect that it's impossible to borrow both their own transforms argument, and the CollisionWorld.

Also, considering the situation where the CollisionWorld and another system parameter needs to borrow CompMut<Transform>, there is no good workaround.


This is partially just a limitation of the way borrowing works. The issue is "solved" in Bevy using ParamSets which usually feels un-ergonomic, but again, there's only so much we can do in Rust, where we must make our borrowing intentions clear at compile time. We can't have two mutable references to the Transform components at the same time. The only way around this is to delay the actual borrowing of the components, requiring an extra lock()/borrow() step.

Maybe we make a Defer system parameter, that wraps around other system parameters, deferring the borrow, and requiring an extra borrow() call to do runtime borrow checking later in the function.

In that case you would be allowed to have transforms: Defer<CompMut<Transform>> and collision_world: Defer<CollisionWorld> in your system params, but you would have to .borrow() them before you could use them, and you wouldn't be able to .borrow() them at the same time without a panic at runtime.


Finally, another alternative, is to have CollisionWorld use it's own component to represent the entity positions, and this component must be synchronized somehow with the Transform component. This is also a foot-gun because it's easy to forget to synchronize the values, and that there is in fact a separate CollisionWorld version of the entity position.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.