diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..24a8e87939aa53cdd833f6be7610cb4972e063ad
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.png filter=lfs diff=lfs merge=lfs -text
diff --git a/Cargo.lock b/Cargo.lock
index a8d012664b7c0f972894c0b10acce9d20698e346..fd483eaacec4653e5b991c28121a8ccc0943131f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -731,6 +731,19 @@ version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c74fcf37593a0053f539c3b088f34f268cbefed031d8eb8ff0fb10d175160242"
 
+[[package]]
+name = "bevy_rapier2d"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e8ffc72387774ca14a98c8c820a32d9d9f37b33c4c862362e89cbfde1620bfa"
+dependencies = [
+ "bevy",
+ "bitflags 1.3.2",
+ "log",
+ "nalgebra",
+ "rapier2d",
+]
+
 [[package]]
 name = "bevy_reflect"
 version = "0.11.0"
@@ -1372,6 +1385,20 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crossbeam"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c"
+dependencies = [
+ "cfg-if",
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "crossbeam-channel"
 version = "0.5.8"
@@ -1382,6 +1409,40 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.16"
@@ -1486,6 +1547,12 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
 
+[[package]]
+name = "either"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+
 [[package]]
 name = "encase"
 version = "0.6.1"
@@ -1657,6 +1724,7 @@ version = "0.1.0"
 dependencies = [
  "bevy",
  "bevy_embedded_assets",
+ "bevy_rapier2d",
  "micro_banimate",
  "micro_bevy_web_utils",
  "micro_bevy_world_utils",
@@ -2161,6 +2229,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 = "libudev-sys"
 version = "0.1.4"
@@ -2214,12 +2288,31 @@ dependencies = [
  "regex-automata 0.1.10",
 ]
 
+[[package]]
+name = "matrixmultiply"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77"
+dependencies = [
+ "autocfg",
+ "rawpointer",
+]
+
 [[package]]
 name = "memchr"
 version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
+[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "metal"
 version = "0.24.0"
@@ -2380,6 +2473,34 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "nalgebra"
+version = "0.32.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "307ed9b18cc2423f29e83f84fd23a8e73628727990181f18641a8b5dc2ab1caa"
+dependencies = [
+ "approx",
+ "glam",
+ "matrixmultiply",
+ "nalgebra-macros",
+ "num-complex 0.4.3",
+ "num-rational 0.4.1",
+ "num-traits",
+ "simba",
+ "typenum",
+]
+
+[[package]]
+name = "nalgebra-macros"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91761aed67d03ad966ef783ae962ef9bbaca728d2dd7ceb7939ec110fffad998"
+dependencies = [
+ "proc-macro2 1.0.66",
+ "quote 1.0.31",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "ndk"
 version = "0.7.0"
@@ -2492,7 +2613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36"
 dependencies = [
  "num-bigint",
- "num-complex",
+ "num-complex 0.2.4",
  "num-integer",
  "num-iter",
  "num-rational 0.2.4",
@@ -2520,6 +2641,15 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-complex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "num-derive"
 version = "0.3.3"
@@ -2582,6 +2712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
 dependencies = [
  "autocfg",
+ "libm",
 ]
 
 [[package]]
@@ -2718,6 +2849,12 @@ version = "1.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
 
+[[package]]
+name = "optional"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978aa494585d3ca4ad74929863093e87cac9790d81fe7aba2b3dc2890643a0fc"
+
 [[package]]
 name = "orbclient"
 version = "0.3.45"
@@ -2771,6 +2908,27 @@ dependencies = [
  "windows-targets 0.48.1",
 ]
 
+[[package]]
+name = "parry2d"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "104ae65232e20477a98f9f1e75ca9850eae24a2ea846a2b1a0af03ad752136ce"
+dependencies = [
+ "approx",
+ "arrayvec",
+ "bitflags 1.3.2",
+ "downcast-rs",
+ "either",
+ "nalgebra",
+ "num-derive",
+ "num-traits",
+ "rustc-hash",
+ "simba",
+ "slab",
+ "smallvec",
+ "spade",
+]
+
 [[package]]
 name = "paste"
 version = "1.0.14"
@@ -2907,12 +3065,40 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab"
 
+[[package]]
+name = "rapier2d"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f94d294a9b96694c14888dd0e8ce77620dcc4f2f49264109ef835fa5e2285b84"
+dependencies = [
+ "approx",
+ "arrayvec",
+ "bit-vec",
+ "bitflags 1.3.2",
+ "crossbeam",
+ "downcast-rs",
+ "instant",
+ "nalgebra",
+ "num-derive",
+ "num-traits",
+ "parry2d",
+ "rustc-hash",
+ "simba",
+ "vec_map",
+]
+
 [[package]]
 name = "raw-window-handle"
 version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
 
+[[package]]
+name = "rawpointer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
+
 [[package]]
 name = "rectangle-pack"
 version = "0.4.2"
@@ -2996,6 +3182,12 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "robust"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5864e7ef1a6b7bcf1d6ca3f655e65e724ed3b52546a0d0a663c991522f552ea"
+
 [[package]]
 name = "rodio"
 version = "0.17.1"
@@ -3046,6 +3238,15 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
 
+[[package]]
+name = "safe_arch"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354"
+dependencies = [
+ "bytemuck",
+]
+
 [[package]]
 name = "same-file"
 version = "1.0.6"
@@ -3107,6 +3308,19 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
 
+[[package]]
+name = "simba"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae"
+dependencies = [
+ "approx",
+ "num-complex 0.4.3",
+ "num-traits",
+ "paste",
+ "wide",
+]
+
 [[package]]
 name = "simd-adler32"
 version = "0.3.5"
@@ -3149,6 +3363,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "spade"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88e65803986868d2372c582007c39ba89936a36ea5f236bf7a7728dc258f04f9"
+dependencies = [
+ "num-traits",
+ "optional",
+ "robust",
+ "smallvec",
+]
+
 [[package]]
 name = "spirv"
 version = "0.2.0+1.5.4"
@@ -3416,6 +3642,12 @@ dependencies = [
  "static_assertions",
 ]
 
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.11"
@@ -3677,6 +3909,16 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "wide"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa469ffa65ef7e0ba0f164183697b89b854253fd31aeb92358b7b6155177d62f"
+dependencies = [
+ "bytemuck",
+ "safe_arch",
+]
+
 [[package]]
 name = "widestring"
 version = "1.0.2"
diff --git a/Cargo.toml b/Cargo.toml
index 83a0556dec0bf4fde0db64089571b928463c5db1..bed5ea35acf3f2e09d8575ad6c7dafb0493ae9b9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,3 +21,4 @@ micro_bevy_web_utils = "0.3.0"
 micro_ldtk = { version = "0.6.1", default-features = false, features = ["ldtk_1_3_0", "autotile"] }
 micro_banimate = { git = "https://lab.lcr.gr/microhacks/micro-banimate.git", rev = "33e56278471f32cbcd1a843aca83298c8b80c7e3" }
 micro_musicbox = "0.7.0"
+bevy_rapier2d = { version = "0.22.0", features = ["simd-stable", "wasm-bindgen"]}
\ No newline at end of file
diff --git a/assets/animations/blob.anim.json b/assets/animations/blob.anim.json
new file mode 100644
index 0000000000000000000000000000000000000000..212909796592d5399f33d98e1b169212a8ff35af
--- /dev/null
+++ b/assets/animations/blob.anim.json
@@ -0,0 +1,38 @@
+{
+  "idle": {
+	"frames": [3],
+	"frame_secs": 10
+  },
+  "crouch_right_down": {
+	"frames": [2],
+	"frame_secs": 10
+  },
+  "crouch_left_down": {
+	"frames": [19],
+	"frame_secs": 10
+  },
+  "walk_right_down": {
+	"frames": [9, 10],
+	"frame_secs": 0.2
+  },
+  "walk_left_down": {
+	"frames": [11, 12],
+	"frame_secs": 0.2
+  },
+  "crouch_right_up": {
+	"frames": [2],
+	"frame_secs": 10
+  },
+  "crouch_left_up": {
+	"frames": [19],
+	"frame_secs": 10
+  },
+  "walk_right_up": {
+	"frames": [9, 10],
+	"frame_secs": 0.2
+  },
+  "walk_left_up": {
+	"frames": [11, 12],
+	"frame_secs": 0.2
+  }
+}
\ No newline at end of file
diff --git a/assets/sprites/beige_blob.png b/assets/sprites/beige_blob.png
new file mode 100644
index 0000000000000000000000000000000000000000..d07a6e4ce6fa596e51d0b3b4e0344f185e5e4c9d
--- /dev/null
+++ b/assets/sprites/beige_blob.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3953c3edd82cd7ab3a739991b530e509122541ca4c4e2e344a64abfd7d32ef5b
+size 112312
diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml
index 971b2ed33b0f3563df4d50bada6d8dd260be2b68..95c541ca8f5679041f528c51fb34633ce32a7662 100644
--- a/game_core/Cargo.toml
+++ b/game_core/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
 [features]
 default = []
 embed = ["dep:bevy_embedded_assets"]
+phys-debug = []
 
 [dependencies]
 bevy.workspace = true
@@ -16,3 +17,4 @@ micro_bevy_web_utils.workspace = true
 micro_ldtk.workspace = true
 micro_banimate.workspace = true
 micro_musicbox.workspace = true
+bevy_rapier2d.workspace = true
\ No newline at end of file
diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs
index 1cab44c147a86bde51b0bb7c5ef74beb0914e49d..bf9a057ba120d5a291a93309db6541a8772ad80c 100644
--- a/game_core/src/assets/startup.rs
+++ b/game_core/src/assets/startup.rs
@@ -12,7 +12,14 @@ pub fn start_preload_resources(
 	next_state.set(AppState::Setup);
 }
 
-pub fn start_load_resources(mut loader: AssetTypeLoader) {}
+pub fn start_load_resources(mut loader: AssetTypeLoader) {
+	loader.load_animation(&[("animations/blob.anim.json", "blob")]);
+
+	loader.load_spritesheet(
+		&SpriteSheetConfig::rectangles(128, 256, 11, 2),
+		&[("sprites/beige_blob.png", "beige_blob")],
+	);
+}
 
 pub fn check_load_resources(loader: AssetTypeLoader, mut next_state: ResMut<NextState<AppState>>) {
 	let load_states = loader.get_all_load_state();
diff --git a/game_core/src/entities/mod.rs b/game_core/src/entities/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a25fe93b22ad8b77f7d89c34f4ee3c0e85024026
--- /dev/null
+++ b/game_core/src/entities/mod.rs
@@ -0,0 +1,15 @@
+mod player;
+
+mod _plugin {
+	use bevy::app::PluginGroupBuilder;
+	use bevy::prelude::PluginGroup;
+
+	pub struct EntityPluginSet;
+	impl PluginGroup for EntityPluginSet {
+		fn build(self) -> PluginGroupBuilder {
+			PluginGroupBuilder::start::<Self>().add(super::player::PlayerSetupPlugin)
+		}
+	}
+}
+
+pub use _plugin::EntityPluginSet;
diff --git a/game_core/src/entities/player.rs b/game_core/src/entities/player.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0d81e7daed8d47398cbf8b79d00df82e72a61644
--- /dev/null
+++ b/game_core/src/entities/player.rs
@@ -0,0 +1,31 @@
+use crate::assets::AssetHandles;
+use crate::system::AppState;
+use bevy::prelude::*;
+
+use bevy_rapier2d::prelude::*;
+
+pub fn spawn_player(mut commands: Commands, assets: Res<AssetHandles>) {
+	commands
+		.spawn((SpriteSheetBundle {
+			sprite: TextureAtlasSprite::new(3),
+			texture_atlas: assets.atlas("beige_blob"),
+			..Default::default()
+		},))
+		.with_children(|builder| {
+			builder.spawn((
+				TransformBundle::from_transform(Transform::from_xyz(0.0, -32.0, 0.0)),
+				Collider::capsule(Vec2::new(0.0, 8.0), Vec2::new(0.0, -48.0), 48.0),
+				KinematicCharacterController {
+					offset: CharacterLength::Absolute(0.01),
+					..Default::default()
+				},
+			));
+		});
+}
+
+pub struct PlayerSetupPlugin;
+impl Plugin for PlayerSetupPlugin {
+	fn build(&self, app: &mut App) {
+		app.add_systems(OnEnter(AppState::InGame), spawn_player);
+	}
+}
diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs
index ac77f6383d401844e7ed78028f18fca9b16b4877..371b2e70e463b79c32995cdb7f32695a4f3fe76a 100644
--- a/game_core/src/lib.rs
+++ b/game_core/src/lib.rs
@@ -1 +1,3 @@
+pub mod assets;
+pub mod entities;
 pub mod system;
diff --git a/game_core/src/main.rs b/game_core/src/main.rs
index c6f275b298886a5da0c16cc3b1bc82dcdfe18619..ef41de509ec2a84276b781447bf48a85c3a0162e 100644
--- a/game_core/src/main.rs
+++ b/game_core/src/main.rs
@@ -2,6 +2,14 @@ use bevy::prelude::App;
 
 fn main() {
 	App::new()
+		.add_plugins(game_core::assets::AssetLoadingPlugin)
 		.add_plugins(game_core::system::SystemPluginSet)
+		.add_plugins(game_core::entities::EntityPluginSet)
+		.add_plugins(bevy_rapier2d::plugin::RapierPhysicsPlugin::<()>::pixels_per_meter(128.0))
+		.add_plugins(micro_banimate::BanimatePluginGroup)
+		.add_plugins(micro_musicbox::CombinedAudioPlugins::<
+			game_core::assets::AssetHandles,
+		>::new())
+		.add_plugins(micro_ldtk::MicroLDTKPlugin)
 		.run();
 }
diff --git a/game_core/src/system/camera.rs b/game_core/src/system/camera.rs
new file mode 100644
index 0000000000000000000000000000000000000000..18899943461824da6a7fc9e1e5b4619402f8cde1
--- /dev/null
+++ b/game_core/src/system/camera.rs
@@ -0,0 +1,98 @@
+use bevy::app::App;
+use bevy::core_pipeline::clear_color::ClearColorConfig;
+use bevy::math::{Vec2, Vec3Swizzles};
+use bevy::prelude::*;
+use bevy::render::camera::ScalingMode;
+use bevy::render::view::RenderLayers;
+
+use crate::system::flow::AppState;
+use crate::system::virtual_size;
+
+/// A flag component to indicate which entity should be followed by the camera
+#[derive(Component, Default)]
+pub struct ChaseCam;
+/// A flag component to indicate a camera that should be used for rendering world entities and sprites
+#[derive(Component, Default)]
+pub struct GameCamera;
+
+/// System that creates a default orthographic camera, with correct tags for querying
+pub fn spawn_orthographic_camera(mut commands: Commands) {
+	spawn_camera(&mut commands, (GameCamera,), 0, ClearColorConfig::Default);
+}
+
+pub fn spawn_camera(
+	commands: &mut Commands,
+	tags: impl Bundle,
+	order: isize,
+	clear_color: ClearColorConfig,
+) -> Entity {
+	let (target_width, target_height) = virtual_size();
+
+	commands
+		.spawn((
+			Camera2dBundle {
+				camera: Camera {
+					order,
+					..Default::default()
+				},
+				projection: OrthographicProjection {
+					area: Rect::new(
+						-(target_width / 2.0),
+						-(target_height / 2.0),
+						(target_width / 2.0),
+						(target_height / 2.0),
+					),
+					scaling_mode: ScalingMode::AutoMin {
+						min_width: target_width,
+						min_height: target_height,
+					},
+					..Default::default()
+				},
+				camera_2d: Camera2d { clear_color },
+				..Default::default()
+			},
+			RenderLayers::layer(order.max(0).min(RenderLayers::TOTAL_LAYERS as isize - 1) as u8),
+			tags,
+		))
+		.id()
+}
+
+/// System that takes the average location of all chase camera entities, and updates the location
+/// of all world cameras to track the average location.
+///
+/// e.g. If a player entity is chased, and a mouse tracking entity is chased, the world cameras will
+/// by updated to the midpoint between the player and the mouse
+pub fn sync_chase_camera_location(
+	mut commands: Commands,
+	chased_query: Query<&Transform, With<ChaseCam>>,
+	camera_query: Query<(Entity, &Transform), With<GameCamera>>,
+) {
+	let mut average_location = Vec2::new(0.0, 0.0);
+	let mut count = 0;
+	for location in chased_query.iter() {
+		average_location += location.translation.xy();
+		count += 1;
+	}
+
+	if count > 0 {
+		average_location /= count as f32;
+	}
+
+	for (entity, location) in camera_query.iter() {
+		commands.entity(entity).insert(Transform {
+			translation: average_location.extend(location.translation.z),
+			..*location
+		});
+	}
+}
+
+/// A marker struct for spawning and managing cameras. Cameras will be created on startup, and
+/// will constantly have their positions synced
+pub struct CameraManagementPlugin;
+
+impl Plugin for CameraManagementPlugin {
+	fn build(&self, app: &mut App) {
+		app.add_systems(OnEnter(AppState::Preload), spawn_orthographic_camera)
+			.add_systems(Update, sync_chase_camera_location);
+	}
+}
diff --git a/game_core/src/system/mod.rs b/game_core/src/system/mod.rs
index c1cc2b49f1736253c4d11cc204fee5bb2a90cfe7..f8823a1a35381351ddc35538371c36968fb300c2 100644
--- a/game_core/src/system/mod.rs
+++ b/game_core/src/system/mod.rs
@@ -1,3 +1,4 @@
+mod camera;
 mod flow;
 mod resource_config;
 mod resources;
@@ -6,17 +7,48 @@ mod web;
 mod _plugin {
 	use crate::system::resources::InitAppPlugins;
 	use bevy::app::PluginGroupBuilder;
-	use bevy::prelude::PluginGroup;
+	use bevy::prelude::*;
+
+	#[cfg(feature = "phys-debug")]
+	mod phys_debug {
+		use bevy::prelude::*;
+		use bevy_rapier2d::prelude::{DebugRenderMode, DebugRenderStyle};
+
+		pub struct PhysDebugPlugin;
+		impl Plugin for PhysDebugPlugin {
+			fn build(&self, app: &mut App) {
+				app.add_plugins(bevy_rapier2d::render::RapierDebugRenderPlugin {
+					mode: DebugRenderMode::all(),
+					style: DebugRenderStyle::default(),
+					enabled: true,
+				});
+			}
+		}
+	}
 
 	pub struct SystemPluginSet;
 	impl PluginGroup for SystemPluginSet {
 		fn build(self) -> PluginGroupBuilder {
-			InitAppPlugins.build().add(super::flow::FlowPlugin)
+			let plugins = InitAppPlugins
+				.build()
+				.add(super::flow::FlowPlugin)
+				.add(super::camera::CameraManagementPlugin);
+
+			#[cfg(feature = "phys-debug")]
+			{
+				plugins.add(phys_debug::PhysDebugPlugin)
+			}
+
+			#[cfg(not(feature = "phys-debug"))]
+			{
+				plugins
+			}
 		}
 	}
 }
 
 pub use _plugin::SystemPluginSet;
+pub use camera::{ChaseCam, GameCamera};
 pub use flow::{run_in_game, run_in_menu, run_in_setup, run_in_splash, AppState};
 pub use resource_config::{get_asset_path_string, initial_size, virtual_size};
 pub use resources::configure_default_plugins;
diff --git a/game_core/src/system/resource_config.rs b/game_core/src/system/resource_config.rs
index 91b314321077acc57ef97571bcc65cdd97554177..d6ba2d556ba71a51b037324536cc30fac6ab4c9c 100644
--- a/game_core/src/system/resource_config.rs
+++ b/game_core/src/system/resource_config.rs
@@ -1,4 +1,4 @@
-const WINDOW_SCALER: f32 = 2.0;
+const WINDOW_SCALER: f32 = 1.0;
 
 #[cfg(not(target_arch = "wasm32"))]
 mod setup {
@@ -17,7 +17,7 @@ mod setup {
 		(1920.0, 1080.0)
 	}
 	pub fn virtual_size() -> (f32, f32) {
-		(1280.0 / WINDOW_SCALER, 720.0 / WINDOW_SCALER)
+		(1920.0 / WINDOW_SCALER, 1080.0 / WINDOW_SCALER)
 	}
 }
 
@@ -29,12 +29,12 @@ mod setup {
 		String::from("assets")
 	}
 	pub fn virtual_size() -> (f32, f32) {
-		(1280.0 / WINDOW_SCALER, 720.0 / WINDOW_SCALER)
+		(1920.0 / WINDOW_SCALER, 1080.0 / WINDOW_SCALER)
 	}
 	#[cfg(feature = "no_aspect")]
 	pub fn initial_size() -> (f32, f32) {
-		static default_width: f32 = 1280.0;
-		static default_height: f32 = 720.0;
+		static default_width: f32 = 1920.0;
+		static default_height: f32 = 1080.0;
 
 		web_sys::window()
 			.and_then(|window: web_sys::Window| {
@@ -56,9 +56,9 @@ mod setup {
 
 	#[cfg(not(feature = "no_aspect"))]
 	pub fn initial_size() -> (f32, f32) {
-		static default_width: f32 = 1280.0;
-		static default_height: f32 = 720.0;
-		static ratio: f32 = 1280.0 / 720.0;
+		static default_width: f32 = 1920.0;
+		static default_height: f32 = 1080.0;
+		static ratio: f32 = 1920.0 / 1080.0;
 
 		web_sys::window()
 			.and_then(|window: web_sys::Window| {
diff --git a/game_core/src/system/resources.rs b/game_core/src/system/resources.rs
index a2b4c02a6529d031ad1c7e3d873c9d589515be56..a60ba9aec094377b1677edf1cab75e4c697dedcc 100644
--- a/game_core/src/system/resources.rs
+++ b/game_core/src/system/resources.rs
@@ -10,7 +10,7 @@ use crate::system::{get_asset_path_string, initial_size};
 pub struct DefaultResourcesPlugin;
 impl Plugin for DefaultResourcesPlugin {
 	fn build(&self, app: &mut App) {
-		app.insert_resource(Msaa::Off)
+		app.insert_resource(Msaa::Sample4)
 			.insert_resource(ClearColor(Color::hex("040720").unwrap()));
 	}
 }
@@ -33,12 +33,12 @@ pub fn configure_default_plugins() -> PluginGroupBuilder {
 			asset_folder: get_asset_path_string(),
 			watch_for_changes: ChangeWatcher::with_delay(Duration::from_secs(1)),
 		})
-		.set(ImagePlugin::default_nearest())
+		.set(ImagePlugin::default_linear())
 		.set(LogPlugin {
 			filter: String::from(
 				"info,game_core=debug,symphonia_core=warn,symphonia_format_ogg=warn,winit=warn,symphonia_bundle_mp3=warn,wgpu_core=warn,wgpu_hal=warn",
 			),
-			level: Level::DEBUG,
+			level: if cfg!(debug_assertions) { Level::DEBUG } else { Level::WARN },
 		});
 
 	#[cfg(feature = "embed")]