working state

This commit is contained in:
Rowan 2024-05-21 16:11:02 -04:00
commit 399198a76a
35 changed files with 6372 additions and 0 deletions

3
.cargo/config.toml Normal file
View file

@ -0,0 +1,3 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

4879
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

34
Cargo.toml Normal file
View file

@ -0,0 +1,34 @@
[package]
name = "brackeys-game"
version = "0.1.0"
edition = "2021"
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 3
[profile.release]
lto = true
opt-level = 3
codegen-units = 1
incremental = false
debug = false
[dependencies]
bevy = "0.13.2"
bevy_asset_loader = "0.20.2"
bevy_editor_pls = "0.8.1"
leafwing-input-manager = "0.13.3"
# Use unstable version for Bevy 0.13 support
bevy_ecs_ldtk = { git = "https://github.com/Trouv/bevy_ecs_ldtk.git" }
bevy_xpbd_2d = "0.4.2"
[patch.crates-io]
# Patch unstable version to resolve conflicting dependencies from bevy_ecs_ldtk
bevy_ecs_tilemap = { git = "https://github.com/StarArawn/bevy_ecs_tilemap" }
[dev-dependencies]
bevy = { version = "0.13.2", features = ["dynamic_linking"] }

Binary file not shown.

View file

@ -0,0 +1,26 @@
All assets in the pack have been repackaged and many have been modified by Brackeys.
LICENSE for all assets:
Creative Commons Zero (CC0)
CREDIT:
SPRITES by analogStudios_:
knight (https://analogstudios.itch.io/camelot)
slime (https://analogstudios.itch.io/dungeonsprites)
platforms and coin (https://analogstudios.itch.io/four-seasons-platformer-sprites)
SPRITES by RottingPixels:
world_tileset and fruit (https://rottingpixels.itch.io/four-seasons-platformer-tileset-16x16free)
WORLD TILESET originally
SOUNDS by Brackeys, Asbjørn Thirslund
MUSIC by Brackeys, Sofia Thirslund
FONTS by Jayvee Enaguas - HarvettFox96 - (https://www.dafont.com/pixel-operator.font?l[]=10&l[]=1)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

786
assets/level.ldtk Normal file

File diff suppressed because one or more lines are too long

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

27
src/assets/ldtk.rs Normal file
View file

@ -0,0 +1,27 @@
use bevy::prelude::*;
use bevy_ecs_ldtk::prelude::*;
use crate::GameState;
pub struct LdtkPlugin;
#[derive(Default, Resource)]
pub struct WorldConfig {
pub path: String,
}
impl Plugin for LdtkPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<WorldConfig>()
.add_plugins(bevy_ecs_ldtk::LdtkPlugin)
.add_systems(OnEnter(GameState::Loading), spawn_world)
.insert_resource(LevelSelection::Uid(0));
}
}
fn spawn_world(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<WorldConfig>) {
commands.spawn(LdtkWorldBundle {
ldtk_handle: asset_server.load(&config.path),
..default()
});
}

13
src/assets/mod.rs Normal file
View file

@ -0,0 +1,13 @@
use bevy::app::Plugin;
use self::ldtk::LdtkPlugin;
pub mod ldtk;
pub struct AssetPlugin;
impl Plugin for AssetPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_plugins(LdtkPlugin);
}
}

92
src/camera/mod.rs Normal file
View file

@ -0,0 +1,92 @@
use bevy::prelude::*;
use bevy_ecs_ldtk::prelude::*;
#[derive(Component, Debug)]
pub struct Follow {
pub target: Entity,
pub offset: Vec3,
}
impl Follow {
pub fn new(target: Entity) -> Self {
Self {
target,
offset: Vec3::ZERO,
}
}
pub fn with_offset(self, offset: Vec3) -> Self {
Self { offset, ..self }
}
}
// link this to the camera's target
// https://trouv.github.io/bevy_ecs_ldtk/latest/how-to-guides/create-bevy-relations-from-ldtk-entity-references.html
#[derive(Debug, Default, Deref, DerefMut, Component)]
pub struct UnresolvedTargetRef(Option<EntityIid>);
impl UnresolvedTargetRef {
pub fn from_field(instance: &EntityInstance) -> UnresolvedTargetRef {
Self(
instance
.get_maybe_entity_ref_field("Target")
.expect("expected entity to have mother entity ref field")
.as_ref()
.map(|entity_ref| EntityIid::new(entity_ref.entity_iid.clone())),
)
}
}
#[derive(Bundle, Default, LdtkEntity)]
pub struct CameraEntity {
#[with(UnresolvedTargetRef::from_field)]
unresolved_target_ref: UnresolvedTargetRef,
camera_2d_bundle: Camera2dBundle
}
pub struct CameraPlugin;
impl Plugin for CameraPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, (follow_target, resolve_target_references, dynamic_scale)).register_ldtk_entity::<CameraEntity>("Camera");
}
}
fn follow_target(
mut follower_q: Query<(&mut Transform, &Follow)>,
target_q: Query<&GlobalTransform, Without<Follow>>,
) {
for (mut transform, follow) in follower_q.iter_mut() {
if let Ok(target) = target_q.get(follow.target) {
transform.translation = target.transform_point(follow.offset);
}
}
}
pub fn resolve_target_references(
mut commands: Commands,
unresolved_targets: Query<(Entity, &UnresolvedTargetRef), Added<UnresolvedTargetRef>>,
ldtk_entities: Query<(Entity, &EntityIid)>,
) {
for (child_entity, unresolved_target_ref) in unresolved_targets.iter() {
if let Some(target_iid) = unresolved_target_ref.0.as_ref() {
let (target_entity, _) = ldtk_entities
.iter()
.find(|(_, iid)| *iid == target_iid)
.expect("enemy's target entity should exist");
commands
.entity(child_entity)
.remove::<UnresolvedTargetRef>()
.insert(Follow::new(target_entity));
} else {
commands
.entity(child_entity)
.remove::<UnresolvedTargetRef>();
}
}
}
pub fn dynamic_scale(mut query: Query<&mut OrthographicProjection>) {
for mut projection in query.iter_mut() {
projection.scale = 0.2;
}
}

11
src/debug/mod.rs Normal file
View file

@ -0,0 +1,11 @@
use bevy::app::Plugin;
use bevy_editor_pls::EditorPlugin;
use bevy_xpbd_2d::plugins::PhysicsDebugPlugin;
pub struct DebugPlugin;
impl Plugin for DebugPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_plugins((EditorPlugin::default(), PhysicsDebugPlugin::default()));
}
}

35
src/level/mod.rs Normal file
View file

@ -0,0 +1,35 @@
use bevy::{input::InputPlugin, prelude::*};
use bevy_ecs_ldtk::prelude::*;
use bevy_xpbd_2d::prelude::*;
#[derive(Component, Default)]
pub struct Tile;
#[derive(Bundle)]
pub struct TileBundle {
tile: Tile,
collider: Collider,
rigidbody: RigidBody,
}
impl Default for TileBundle {
fn default() -> Self {
Self {
tile: Tile,
collider: Collider::rectangle(16., 16.),
rigidbody: RigidBody::Static,
}
}
}
#[derive(Default, Bundle, LdtkIntCell)]
pub struct TileIntCell {
tile_bundle: TileBundle,
}
pub struct LevelPlugin;
impl Plugin for LevelPlugin {
fn build(&self, app: &mut App) {
app.register_default_ldtk_int_cell_for_layer::<TileIntCell>("Ground");
}
}

55
src/lib.rs Normal file
View file

@ -0,0 +1,55 @@
use assets::{ldtk::WorldConfig, AssetPlugin};
use bevy::prelude::*;
use bevy_xpbd_2d::math::{Scalar, Vector};
use camera::CameraPlugin;
use debug::DebugPlugin;
use level::LevelPlugin;
use physics::PhysicsPlugin;
use player::PlayerPlugin;
mod assets;
mod camera;
mod debug;
mod level;
mod physics;
mod player;
#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
pub enum GameState {
MainMenu,
#[default]
Loading,
Playing,
}
pub const GRAVITY: Scalar = 9.81;
pub const GRAVITY_VECTOR: Vector = Vector { x: 0., y: -GRAVITY };
pub struct GamePlugin;
impl Plugin for GamePlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.add_plugins((
AssetPlugin,
PhysicsPlugin,
LevelPlugin,
PlayerPlugin,
CameraPlugin,
))
.init_state::<GameState>()
.insert_resource(WorldConfig {
path: "level.ldtk".to_string(),
});
#[cfg(debug_assertions)]
app.add_plugins(DebugPlugin);
}
}
fn spawn_camera(mut commands: Commands) {
let mut camera = Camera2dBundle::default();
camera.projection.scale = 0.5;
camera.transform.translation.x += 1280.0 / 4.0;
camera.transform.translation.y += 720.0 / 4.0;
commands.spawn(camera);
}

6
src/main.rs Normal file
View file

@ -0,0 +1,6 @@
use bevy::prelude::*;
use brackeys_game::GamePlugin;
fn main() {
App::new().add_plugins(GamePlugin).run();
}

8
src/physics/mod.rs Normal file
View file

@ -0,0 +1,8 @@
use bevy::app::Plugin;
pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_plugins(bevy_xpbd_2d::prelude::PhysicsPlugins::default());
}
}

292
src/player/controller.rs Normal file
View file

@ -0,0 +1,292 @@
use bevy::prelude::*;
use bevy_xpbd_2d::{
math::{Scalar, Vector, PI},
prelude::*,
SubstepSchedule, SubstepSet,
};
use crate::GRAVITY_VECTOR;
#[derive(Component, Reflect)]
#[component(storage = "SparseSet")]
pub struct Grounded;
#[derive(Component, Reflect)]
pub struct JumpImpulse(Scalar);
impl Default for JumpImpulse {
fn default() -> Self {
JumpImpulse(7.)
}
}
#[derive(Component, Reflect)]
pub struct MaxJumpDuration(Scalar);
impl Default for MaxJumpDuration {
fn default() -> Self {
MaxJumpDuration(0.1)
}
}
#[derive(Component, Reflect)]
pub struct MovementAcceleration(Scalar);
impl Default for MovementAcceleration {
fn default() -> Self {
MovementAcceleration(75.)
}
}
#[derive(Component, Reflect)]
pub struct MovementDampingFactor(Scalar);
impl Default for MovementDampingFactor {
fn default() -> Self {
MovementDampingFactor(0.9)
}
}
#[derive(Component, Reflect)]
pub struct MaxSlopeAngle(Scalar);
impl Default for MaxSlopeAngle {
fn default() -> Self {
MaxSlopeAngle(PI * 0.45)
}
}
#[derive(Component, Default, Reflect)]
pub struct CharacterController;
#[derive(Component, Reflect)]
pub struct ControllerGravity(Vector);
impl Default for ControllerGravity {
fn default() -> Self {
ControllerGravity(GRAVITY_VECTOR)
}
}
#[derive(Bundle, Default)]
pub struct MovementBundle {
acceleration: MovementAcceleration,
damping: MovementDampingFactor,
jump_impulse: JumpImpulse,
max_slope_angle: MaxSlopeAngle,
direction: MovementDirection,
}
#[derive(Bundle)]
pub struct CharacterControllerBundle {
character_controller: CharacterController,
rigidbody: RigidBody,
collider: Collider,
ground_caster: ShapeCaster,
gravity: ControllerGravity,
movement: MovementBundle,
locked_axes: LockedAxes,
}
impl Default for CharacterControllerBundle {
fn default() -> Self {
let collider = Collider::rectangle(12., 24.);
let mut caster_shape = collider.clone();
caster_shape.set_scale(Vector::ONE * 0.5, 1);
Self {
locked_axes: LockedAxes::ROTATION_LOCKED,
character_controller: CharacterController,
rigidbody: RigidBody::Kinematic,
collider,
ground_caster: ShapeCaster::new(caster_shape, Vector::ZERO, 0.0, Direction2d::NEG_Y)
.with_max_time_of_impact(1.0),
gravity: ControllerGravity(GRAVITY_VECTOR * 5.),
movement: MovementBundle {
acceleration: MovementAcceleration(420.),
jump_impulse: JumpImpulse(46.),
..default()
},
}
}
}
#[derive(Component, Reflect)]
#[component(storage = "SparseSet")]
pub struct Jumping;
#[derive(Component, Default, Reflect)]
pub struct MovementDirection(pub Scalar);
pub struct CharacterControllerPlugin;
impl Plugin for CharacterControllerPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Grounded>()
.register_type::<MovementAcceleration>()
.register_type::<MovementDampingFactor>()
.register_type::<JumpImpulse>()
.register_type::<MaxSlopeAngle>()
.register_type::<MovementDirection>()
.register_type::<CharacterController>()
.register_type::<ControllerGravity>()
.add_systems(
Update,
(
update_grounded,
apply_gravity,
handle_movement,
handle_jump,
apply_movement_damping,
)
.chain(),
)
.add_systems(
// Run collision handling in substep schedule
SubstepSchedule,
kinematic_controller_collisions.in_set(SubstepSet::SolveUserConstraints),
);
}
}
fn handle_movement(
time: Res<Time>,
mut query: Query<
(
&mut LinearVelocity,
&MovementAcceleration,
&MovementDirection,
),
With<CharacterController>,
>,
) {
let delta = time.delta_seconds();
for (mut velocity, MovementAcceleration(acceleration), MovementDirection(direction)) in
query.iter_mut()
{
velocity.x += direction * acceleration * delta;
}
}
fn handle_jump(
mut query: Query<
(&mut LinearVelocity, &JumpImpulse),
(With<CharacterController>, With<Jumping>, With<Grounded>),
>,
) {
for (mut velocity, JumpImpulse(impulse)) in query.iter_mut() {
velocity.y = *impulse;
}
}
fn apply_movement_damping(mut query: Query<(&MovementDampingFactor, &mut LinearVelocity)>) {
for (damping_factor, mut linear_velocity) in &mut query {
// We could use `LinearDamping`, but we don't want to dampen movement along the Y axis
linear_velocity.x *= damping_factor.0;
}
}
fn update_grounded(
mut commands: Commands,
mut query: Query<
(Entity, &ShapeHits, &Rotation, Option<&MaxSlopeAngle>),
With<CharacterController>,
>,
) {
for (entity, hits, rotation, max_slope_angle) in &mut query {
// The character is grounded if the shape caster has a hit with a normal
// that isn't too steep.
let is_grounded = hits.iter().any(|hit| {
if let Some(angle) = max_slope_angle {
rotation.rotate(-hit.normal2).angle_between(Vector::Y).abs() <= angle.0
} else {
true
}
});
if is_grounded {
commands.entity(entity).insert(Grounded);
} else {
commands.entity(entity).remove::<Grounded>();
}
}
}
fn apply_gravity(time: Res<Time>, mut query: Query<(&mut LinearVelocity, &ControllerGravity)>) {
let delta = time.delta_seconds();
for (mut velocity, ControllerGravity(gravity)) in query.iter_mut() {
velocity.0 += *gravity * delta;
}
}
#[allow(clippy::type_complexity)]
fn kinematic_controller_collisions(
collisions: Res<Collisions>,
collider_parents: Query<&ColliderParent, Without<Sensor>>,
mut character_controllers: Query<
(
&RigidBody,
&mut Position,
&Rotation,
&mut LinearVelocity,
Option<&MaxSlopeAngle>,
),
With<CharacterController>,
>,
) {
// Iterate through collisions and move the kinematic body to resolve penetration
for contacts in collisions.iter() {
// If the collision didn't happen during this substep, skip the collision
if !contacts.during_current_substep {
continue;
}
// Get the rigid body entities of the colliders (colliders could be children)
let Ok([collider_parent1, collider_parent2]) =
collider_parents.get_many([contacts.entity1, contacts.entity2])
else {
continue;
};
// Get the body of the character controller and whether it is the first
// or second entity in the collision.
let is_first: bool;
let (rb, mut position, rotation, mut linear_velocity, max_slope_angle) =
if let Ok(character) = character_controllers.get_mut(collider_parent1.get()) {
is_first = true;
character
} else if let Ok(character) = character_controllers.get_mut(collider_parent2.get()) {
is_first = false;
character
} else {
continue;
};
// This system only handles collision response for kinematic character controllers
if !rb.is_kinematic() {
continue;
}
// Iterate through contact manifolds and their contacts.
// Each contact in a single manifold shares the same contact normal.
for manifold in contacts.manifolds.iter() {
let normal = if is_first {
-manifold.global_normal1(rotation)
} else {
-manifold.global_normal2(rotation)
};
// Solve each penetrating contact in the manifold
for contact in manifold.contacts.iter().filter(|c| c.penetration > 0.0) {
position.0 += normal * contact.penetration;
}
// If the slope isn't too steep to walk on but the character
// is falling, reset vertical velocity.
if max_slope_angle.is_some_and(|angle| normal.angle_between(Vector::Y).abs() <= angle.0)
&& linear_velocity.y < 0.0
{
linear_velocity.y = linear_velocity.y.max(0.0);
}
}
}
}

31
src/player/input.rs Normal file
View file

@ -0,0 +1,31 @@
use bevy::prelude::*;
use leafwing_input_manager::prelude::*;
#[derive(Actionlike, PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
pub enum PlayerAction {
Move,
Jump,
}
impl PlayerAction {
pub fn default_input_map() -> InputMap<Self> {
let mut input_map = InputMap::default();
// Default gamepad input bindings
input_map.insert(Self::Move, DualAxis::left_stick());
input_map.insert(Self::Jump, GamepadButtonType::South);
// Default kbm input bindings
input_map.insert(Self::Move, VirtualDPad::wasd());
input_map.insert(Self::Jump, KeyCode::Space);
input_map
}
}
pub struct InputPlugin;
impl Plugin for InputPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(InputManagerPlugin::<PlayerAction>::default());
}
}

71
src/player/mod.rs Normal file
View file

@ -0,0 +1,71 @@
mod controller;
mod input;
use bevy::prelude::*;
use bevy_ecs_ldtk::prelude::*;
use leafwing_input_manager::{action_state::ActionState, input_map::InputMap, InputManagerBundle};
use self::{
controller::{
CharacterControllerBundle, CharacterControllerPlugin, Jumping, MovementDirection,
},
input::{InputPlugin, PlayerAction},
};
#[derive(Component, Default)]
pub struct Player;
#[derive(Bundle)]
pub struct PlayerBundle {
player: Player,
input_manager_bundle: InputManagerBundle<PlayerAction>,
character_controller_bundle: CharacterControllerBundle,
}
impl Default for PlayerBundle {
fn default() -> Self {
Self {
player: Player,
input_manager_bundle: InputManagerBundle::with_map(PlayerAction::default_input_map()),
character_controller_bundle: CharacterControllerBundle::default(),
}
}
}
#[derive(Default, Bundle, LdtkEntity)]
pub struct PlayerEntity {
player_bundle: PlayerBundle,
#[sprite_sheet_bundle]
sprite_sheet_bundle: SpriteSheetBundle,
}
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((InputPlugin, CharacterControllerPlugin))
.add_systems(Update, (apply_movement_input, apply_jumping_input))
.register_ldtk_entity::<PlayerEntity>("Player");
}
}
fn apply_movement_input(mut query: Query<(&ActionState<PlayerAction>, &mut MovementDirection)>) {
for (action_state, mut direction) in query.iter_mut() {
direction.0 = action_state
.clamped_axis_pair(&PlayerAction::Move)
.map_or(0., |v| v.x())
}
}
fn apply_jumping_input(
mut commands: Commands,
mut query: Query<(Entity, &ActionState<PlayerAction>, Has<Jumping>)>,
) {
for (entity, action_state, is_jumping) in query.iter_mut() {
let wants_to_jump = action_state.pressed(&PlayerAction::Jump);
if wants_to_jump && !is_jumping {
commands.entity(entity).insert(Jumping);
} else if !wants_to_jump && is_jumping {
commands.entity(entity).remove::<Jumping>();
}
}
}