diff --git a/Cargo.lock b/Cargo.lock
index c232af5870425231352896f5ca57058a2145c979..149aa7c3b32da8155e3327b432076bd7a5813c76 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -524,6 +524,18 @@ dependencies = [
  "radsort",
 ]
 
+[[package]]
+name = "bevy_prototype_lyon"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799f9bc78cdef67d8dbecd06d2147d30732cdc74eb5e15edacf76ff2069b3d9f"
+dependencies = [
+ "bevy",
+ "lyon_algorithms",
+ "lyon_tessellation",
+ "svgtypes",
+]
+
 [[package]]
 name = "bevy_ptr"
 version = "0.10.1"
@@ -1148,6 +1160,15 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "float_next_after"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fc612c5837986b7104a87a0df74a5460931f1c5274be12f8d0f40aa2f30d632"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "foreign-types"
 version = "0.3.2"
@@ -1511,6 +1532,12 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "libm"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4"
+
 [[package]]
 name = "lock_api"
 version = "0.4.10"
@@ -1527,6 +1554,48 @@ version = "0.4.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
 
+[[package]]
+name = "lyon_algorithms"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00a0349cd8f0270781bb93a824b63df6178e3b4a27794e7be3ce3763f5a44d6e"
+dependencies = [
+ "lyon_path",
+ "num-traits",
+]
+
+[[package]]
+name = "lyon_geom"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74df1ff0a0147282eb10699537a03baa7d31972b58984a1d44ce0624043fe8ad"
+dependencies = [
+ "arrayvec",
+ "euclid",
+ "num-traits",
+]
+
+[[package]]
+name = "lyon_path"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8358c012e5651e4619cfd0b5b75c0f77866181a01b0909aab4bae14adf660"
+dependencies = [
+ "lyon_geom",
+ "num-traits",
+]
+
+[[package]]
+name = "lyon_tessellation"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d2124218d5428149f9e09520b9acc024334a607e671f032d06567b61008977c"
+dependencies = [
+ "float_next_after",
+ "lyon_path",
+ "thiserror",
+]
+
 [[package]]
 name = "malloc_buf"
 version = "0.0.6"
@@ -1762,6 +1831,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
 dependencies = [
  "autocfg",
+ "libm",
 ]
 
 [[package]]
@@ -2158,6 +2228,7 @@ name = "shoot-the-revival"
 version = "0.1.0"
 dependencies = [
  "bevy",
+ "bevy_prototype_lyon",
  "log",
 ]
 
@@ -2167,6 +2238,12 @@ version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f"
 
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
 [[package]]
 name = "slab"
 version = "0.4.8"
@@ -2235,6 +2312,15 @@ version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
 
+[[package]]
+name = "svgtypes"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22975e8a2bac6a76bb54f898a6b18764633b00e780330f0b689f65afb3975564"
+dependencies = [
+ "siphasher",
+]
+
 [[package]]
 name = "syn"
 version = "1.0.109"
diff --git a/Cargo.toml b/Cargo.toml
index 246fa02efc9185261e37db2323cf911bda7ed39d..82a09c98573acfc64a0791ef90a71e9c04ef5234 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ edition = "2021"
 
 [dependencies]
 log = "0.4.19"
+bevy_prototype_lyon = "0.8.0"
 
 [dependencies.bevy]
 version = "0.10.1"
diff --git a/src/entities/mod.rs b/src/entities/mod.rs
index f604191ddf9abad6a95d780d9c365f8cdd772374..0b8174e00a131672fd1495c24c961e1cab0b3a61 100644
--- a/src/entities/mod.rs
+++ b/src/entities/mod.rs
@@ -18,6 +18,7 @@ mod _plugin {
 			.add_systems((
 				super::player::process_player_input,
 				super::motion::apply_velocity,
+				super::spawning::apply_despawn_boundaries,
 			));
 		}
 	}
@@ -25,6 +26,6 @@ mod _plugin {
 
 pub use _plugin::EntityPlugin;
 pub use collision::{check_box_collisions, BoxSize, CollisionGroup};
-pub use motion::Velocity;
+pub use motion::{Velocity, DIR_LEFT, DIR_RIGHT};
 pub use player::Player;
-pub use spawning::EntitySpawner;
+pub use spawning::{DespawnOffScreen, EntitySpawner};
diff --git a/src/entities/motion.rs b/src/entities/motion.rs
index e4a96d4b899e988775b83cb5abedb87d855dab49..9f2523bfb18ef2213c56e9b5fd1fe8c4bf4ff218 100644
--- a/src/entities/motion.rs
+++ b/src/entities/motion.rs
@@ -3,6 +3,9 @@ use bevy::math::Vec2;
 use bevy::prelude::{Component, Query, Res, Time, Transform};
 use std::ops::Mul;
 
+pub const DIR_LEFT: Vec2 = Vec2::new(-1.0, 0.0);
+pub const DIR_RIGHT: Vec2 = Vec2::new(1.0, 0.0);
+
 #[derive(Clone, Copy, Debug, Component)]
 pub struct Velocity(Vec2);
 deref_as!(mut Velocity => Vec2);
diff --git a/src/entities/player.rs b/src/entities/player.rs
index ab1bd70edb97fcc8fc0823a79c56bbc736722ed4..e86b3bdf7ab337a57dcb73229a4b560af2187d66 100644
--- a/src/entities/player.rs
+++ b/src/entities/player.rs
@@ -1,8 +1,7 @@
-use crate::entities::spawning::EntitySpawner;
-use crate::entities::Velocity;
+use crate::entities::{CollisionGroup, DespawnOffScreen, EntitySpawner, Velocity, DIR_RIGHT};
 use crate::system::window_bounds;
-use bevy::math::Vec2;
-use bevy::prelude::{Component, DetectChangesMut, Input, KeyCode, Query, Res, With};
+use bevy::math::{Vec2, Vec3Swizzles};
+use bevy::prelude::{Component, DetectChangesMut, Input, KeyCode, Query, Res, Transform, With};
 
 #[derive(Component)]
 pub struct Player;
@@ -17,7 +16,9 @@ macro_rules! any_pressed {
 }
 
 pub fn process_player_input(
-	mut player_query: Query<&mut Velocity, With<Player>>,
+	mut spawner: EntitySpawner,
+	mut player_velocity_query: Query<&mut Velocity, With<Player>>,
+	player_position_query: Query<&Transform, With<Player>>,
 	input: Res<Input<KeyCode>>,
 ) {
 	let mut delta = Vec2::default();
@@ -35,7 +36,17 @@ pub fn process_player_input(
 		delta.y += 1.0;
 	}
 
-	for mut velocity in &mut player_query {
+	if input.just_pressed(KeyCode::Space) {
+		for position in &player_position_query {
+			spawner.spawn_projectile(
+				position.translation.xy() + Vec2::new(15.0, 0.0),
+				DIR_RIGHT,
+				(CollisionGroup::FriendlyProjectile, DespawnOffScreen::all()),
+			);
+		}
+	}
+
+	for mut velocity in &mut player_velocity_query {
 		if delta != Vec2::ZERO {
 			*velocity = Velocity::default() * delta;
 		} else if **(velocity.bypass_change_detection()) != Vec2::ZERO {
diff --git a/src/entities/spawning.rs b/src/entities/spawning.rs
index 7dc9980ada4b6f5bb974a4169636579441832668..19165ff3b9a963f1efe2896a6ed9fff496adc884 100644
--- a/src/entities/spawning.rs
+++ b/src/entities/spawning.rs
@@ -1,14 +1,72 @@
 use crate::entities::motion::Velocity;
 use crate::entities::{BoxSize, CollisionGroup, Player};
-use crate::system::{Background, BACKGROUND_HEIGHT, BACKGROUND_WIDTH};
+use crate::system::{Background, DawnBringerPalette, BACKGROUND_HEIGHT, BACKGROUND_WIDTH};
+use crate::utilities::translate_rect;
 use bevy::ecs::system::SystemParam;
-use bevy::prelude::{AssetServer, Commands, Entity, Res, Sprite, SpriteBundle, Transform, Vec2};
+use bevy::math::Vec3Swizzles;
+use bevy::prelude::{
+	AssetServer, Bundle, Changed, Commands, Component, DespawnRecursiveExt, Entity,
+	OrthographicProjection, Query, Rect, Res, Sprite, SpriteBundle, Transform, Vec2,
+};
+use bevy_prototype_lyon::draw::Stroke;
+use bevy_prototype_lyon::prelude::{Fill, GeometryBuilder, ShapeBundle};
+use bevy_prototype_lyon::shapes;
 
 static Z_PLAYER: f32 = 300.0;
 static Z_ITEMS: f32 = 200.0;
 static Z_ENEMY: f32 = 250.0;
 static Z_BACKGROUND: f32 = 50.0;
 
+#[derive(Copy, Clone, Debug, Component)]
+pub struct DespawnOffScreen {
+	left: bool,
+	right: bool,
+	top: bool,
+	bottom: bool,
+}
+
+impl Default for DespawnOffScreen {
+	fn default() -> Self {
+		Self {
+			left: true,
+			right: true,
+			top: true,
+			bottom: true,
+		}
+	}
+}
+
+impl DespawnOffScreen {
+	pub fn all() -> Self {
+		Self::default()
+	}
+
+	pub fn right() -> Self {
+		Self {
+			left: false,
+			right: true,
+			top: false,
+			bottom: false,
+		}
+	}
+
+	pub fn left() -> Self {
+		Self {
+			left: true,
+			right: false,
+			top: false,
+			bottom: false,
+		}
+	}
+
+	pub fn entity_should_despawn(&self, entity_pos: Vec2, camera_bounds: Rect) -> bool {
+		self.left && entity_pos.x < camera_bounds.min.x
+			|| self.top && entity_pos.y > camera_bounds.max.y
+			|| self.right && entity_pos.x > camera_bounds.max.x
+			|| self.bottom && entity_pos.y < camera_bounds.min.y
+	}
+}
+
 #[derive(SystemParam)]
 pub struct EntitySpawner<'w, 's> {
 	commands: Commands<'w, 's>,
@@ -62,4 +120,56 @@ impl<'w, 's> EntitySpawner<'w, 's> {
 			))
 			.id()
 	}
+
+	pub fn spawn_projectile(
+		&mut self,
+		position: Vec2,
+		velocity_direction: Vec2,
+		rest: impl Bundle,
+	) -> Entity {
+		let projectile_shape = shapes::RoundedPolygon {
+			radius: 2.0,
+			closed: true,
+			points: vec![
+				Vec2::new(-7.0, -2.0),
+				Vec2::new(-7.0, 2.0),
+				Vec2::new(7.0, 2.0),
+				Vec2::new(7.0, -2.0),
+			],
+		};
+
+		self.commands
+			.spawn((
+				ShapeBundle {
+					path: GeometryBuilder::build_as(&projectile_shape),
+					transform: Transform::from_translation(position.extend(Z_ITEMS)),
+					..Default::default()
+				},
+				BoxSize::from(Vec2::new(10.0, 10.0)),
+				Fill::color(DawnBringerPalette::MID_BLUE),
+				Stroke::new(DawnBringerPalette::MIDNIGHT_BLUE, 2.0),
+				Velocity::from(Vec2::new(100.0, 0.0) * velocity_direction),
+				rest,
+			))
+			.id()
+	}
+}
+
+pub fn apply_despawn_boundaries(
+	mut commands: Commands,
+	camera_query: Query<(&OrthographicProjection, &Transform)>,
+	bounds_query: Query<(Entity, &DespawnOffScreen, &Transform), Changed<Transform>>,
+) {
+	let (camera_ortho, camera_position) = match camera_query.get_single() {
+		Ok(value) => value,
+		Err(_) => return,
+	};
+
+	let viewport = translate_rect(camera_ortho.area, camera_position.translation.xy());
+
+	for (entity, boundary, transform) in &bounds_query {
+		if boundary.entity_should_despawn(transform.translation.xy(), viewport) {
+			commands.entity(entity).despawn_recursive();
+		}
+	}
 }
diff --git a/src/main.rs b/src/main.rs
index 2f567b59d415c5425bcedbc7c38b6428a59fce8a..4e2a976576e3cf4f96df982446c18d6f406eca1f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -17,5 +17,6 @@ fn main() {
 		}))
 		.add_plugin(shoot_the_revival::system::SystemPlugin)
 		.add_plugin(shoot_the_revival::entities::EntityPlugin)
+		.add_plugin(bevy_prototype_lyon::plugin::ShapePlugin)
 		.run();
 }
diff --git a/src/utilities.rs b/src/utilities.rs
index 9d1010488208d59005b16f472744cb85518087c4..f6826c967020795028c8735a3e72cdb5ad23ec69 100644
--- a/src/utilities.rs
+++ b/src/utilities.rs
@@ -1,3 +1,5 @@
+use bevy::math::Vec2;
+use bevy::prelude::Rect;
 #[macro_export]
 macro_rules! deref_as {
     ($outer:ty => $inner:ty) => {
@@ -18,3 +20,7 @@ macro_rules! deref_as {
 		}
 	}
 }
+
+pub fn translate_rect(rect: Rect, translation: Vec2) -> Rect {
+	Rect::from_corners(rect.min + translation, rect.max + translation)
+}