diff --git a/src/entities/mod.rs b/src/entities/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e587cb61f12e20c4f1f47659683d3a3a746677e0
--- /dev/null
+++ b/src/entities/mod.rs
@@ -0,0 +1,24 @@
+mod motion;
+mod player;
+mod spawning;
+
+mod _plugin {
+	use bevy::app::App;
+	use bevy::prelude::Plugin;
+
+	pub struct EntityPlugin;
+	impl Plugin for EntityPlugin {
+		fn build(&self, app: &mut App) {
+			app.add_startup_system(super::player::spawn_player)
+				.add_systems((
+					super::player::process_player_input,
+					super::motion::apply_velocity,
+				));
+		}
+	}
+}
+
+pub use _plugin::EntityPlugin;
+pub use motion::Velocity;
+pub use player::Player;
+pub use spawning::EntitySpawner;
diff --git a/src/entities/motion.rs b/src/entities/motion.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e4a96d4b899e988775b83cb5abedb87d855dab49
--- /dev/null
+++ b/src/entities/motion.rs
@@ -0,0 +1,41 @@
+use crate::deref_as;
+use bevy::math::Vec2;
+use bevy::prelude::{Component, Query, Res, Time, Transform};
+use std::ops::Mul;
+
+#[derive(Clone, Copy, Debug, Component)]
+pub struct Velocity(Vec2);
+deref_as!(mut Velocity => Vec2);
+
+impl From<Vec2> for Velocity {
+	fn from(value: Vec2) -> Self {
+		Velocity(value)
+	}
+}
+impl Default for Velocity {
+	fn default() -> Self {
+		Self(Vec2::new(100.0, 100.0))
+	}
+}
+
+impl<T> Mul<T> for Velocity
+where
+	Vec2: Mul<T, Output = Vec2>,
+{
+	type Output = Velocity;
+
+	fn mul(self, rhs: T) -> Self::Output {
+		Velocity::from(self.0.mul(rhs))
+	}
+}
+
+impl Velocity {
+	pub const ZERO: Velocity = Velocity(Vec2::ZERO);
+}
+
+pub fn apply_velocity(delta: Res<Time>, mut entity_query: Query<(&Velocity, &mut Transform)>) {
+	let dt = delta.delta_seconds();
+	for (velocity, mut transform) in &mut entity_query {
+		transform.translation += velocity.extend(0.0) * dt;
+	}
+}
diff --git a/src/entities/player.rs b/src/entities/player.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ab1bd70edb97fcc8fc0823a79c56bbc736722ed4
--- /dev/null
+++ b/src/entities/player.rs
@@ -0,0 +1,45 @@
+use crate::entities::spawning::EntitySpawner;
+use crate::entities::Velocity;
+use crate::system::window_bounds;
+use bevy::math::Vec2;
+use bevy::prelude::{Component, DetectChangesMut, Input, KeyCode, Query, Res, With};
+
+#[derive(Component)]
+pub struct Player;
+
+pub fn spawn_player(mut spawner: EntitySpawner) {
+	let bounds = window_bounds();
+	spawner.spawn_player_at(bounds.half_size());
+}
+
+macro_rules! any_pressed {
+	($input: expr, $key: expr $(, $more: expr),*) => {$input.pressed($key) $(|| any_pressed!($input, $more))*};
+}
+
+pub fn process_player_input(
+	mut player_query: Query<&mut Velocity, With<Player>>,
+	input: Res<Input<KeyCode>>,
+) {
+	let mut delta = Vec2::default();
+
+	if any_pressed!(input, KeyCode::A, KeyCode::Left) {
+		delta.x -= 1.0;
+	}
+	if any_pressed!(input, KeyCode::D, KeyCode::Right) {
+		delta.x += 1.0;
+	}
+	if any_pressed!(input, KeyCode::S, KeyCode::Down) {
+		delta.y -= 1.0;
+	}
+	if any_pressed!(input, KeyCode::W, KeyCode::Up) {
+		delta.y += 1.0;
+	}
+
+	for mut velocity in &mut player_query {
+		if delta != Vec2::ZERO {
+			*velocity = Velocity::default() * delta;
+		} else if **(velocity.bypass_change_detection()) != Vec2::ZERO {
+			*velocity = Velocity::ZERO;
+		}
+	}
+}
diff --git a/src/entities/spawning.rs b/src/entities/spawning.rs
new file mode 100644
index 0000000000000000000000000000000000000000..27d3422be0e57cb5a49ce8b267982b1b47b3930e
--- /dev/null
+++ b/src/entities/spawning.rs
@@ -0,0 +1,31 @@
+use crate::entities::motion::Velocity;
+use crate::entities::Player;
+use bevy::ecs::system::SystemParam;
+use bevy::prelude::{AssetServer, Commands, Entity, Res, SpriteBundle, Transform, Vec2};
+
+#[derive(SystemParam)]
+pub struct EntitySpawner<'w, 's> {
+	commands: Commands<'w, 's>,
+	assets: Res<'w, AssetServer>,
+}
+
+static Z_PLAYER: f32 = 300.0;
+static Z_ITEMS: f32 = 200.0;
+static Z_ENEMY: f32 = 250.0;
+static Z_BACKGROUND: f32 = 50.0;
+
+impl<'w, 's> EntitySpawner<'w, 's> {
+	pub fn spawn_player_at(&mut self, position: Vec2) -> Entity {
+		self.commands
+			.spawn((
+				SpriteBundle {
+					texture: self.assets.load("sprites/ship.png"),
+					transform: Transform::from_translation(position.extend(Z_PLAYER)),
+					..Default::default()
+				},
+				Velocity::ZERO,
+				Player,
+			))
+			.id()
+	}
+}
diff --git a/src/lib.rs b/src/lib.rs
index ac77f6383d401844e7ed78028f18fca9b16b4877..f4e402d620461fba88f54e504ad4b05ed1bf327f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1 +1,3 @@
+pub mod entities;
 pub mod system;
+pub mod utilities;
diff --git a/src/main.rs b/src/main.rs
index dfe777435ae9b5c1be0c760990634385d01ffd1e..2f567b59d415c5425bcedbc7c38b6428a59fce8a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,19 +1,6 @@
-use bevy::asset::AssetServer;
-use bevy::prelude::{App, Commands, PluginGroup, Res, Transform, Window, WindowPlugin};
-use bevy::sprite::SpriteBundle;
+use bevy::prelude::{App, PluginGroup, Window, WindowPlugin};
 use bevy::window::{WindowMode, WindowResolution};
 use bevy::DefaultPlugins;
-use shoot_the_revival::system::window_bounds;
-
-fn spawn_ship(mut commands: Commands, assets: Res<AssetServer>) {
-	let bounds = window_bounds();
-
-	commands.spawn(SpriteBundle {
-		texture: assets.load("sprites/ship.png"),
-		transform: Transform::from_translation(bounds.half_size().extend(0.0)),
-		..Default::default()
-	});
-}
 
 fn main() {
 	App::new()
@@ -29,6 +16,6 @@ fn main() {
 			..Default::default()
 		}))
 		.add_plugin(shoot_the_revival::system::SystemPlugin)
-		.add_startup_system(spawn_ship)
+		.add_plugin(shoot_the_revival::entities::EntityPlugin)
 		.run();
 }
diff --git a/src/system/camera.rs b/src/system/camera.rs
index ee29be94e7c4945256d6e325bc37067be52a5662..dd74861d53868e42837c0e9a424c455929930986 100644
--- a/src/system/camera.rs
+++ b/src/system/camera.rs
@@ -29,7 +29,7 @@ pub fn spawn_2d_camera(mut commands: Commands) {
 			camera_2d: Camera2d {
 				clear_color: ClearColorConfig::Custom(Color::WHITE),
 			},
-			transform: Transform::from_translation(bounds.half_size().extend(0.0)),
+			transform: Transform::from_translation(bounds.half_size().extend(1000.0)),
 			..Default::default()
 		},
 	));
diff --git a/src/utilities.rs b/src/utilities.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9d1010488208d59005b16f472744cb85518087c4
--- /dev/null
+++ b/src/utilities.rs
@@ -0,0 +1,20 @@
+#[macro_export]
+macro_rules! deref_as {
+    ($outer:ty => $inner:ty) => {
+		impl ::std::ops::Deref for $outer {
+			type Target = $inner;
+			fn deref(&self) -> &Self::Target {
+				&self.0
+			}
+		}
+	};
+
+	(mut $outer:ty => $inner:ty) => {
+		deref_as!($outer => $inner);
+		impl ::std::ops::DerefMut for $outer {
+			fn deref_mut(&mut self) -> &mut Self::Target {
+				&mut self.0
+			}
+		}
+	}
+}