Merge pull request #2 from griffi-gh/multiplayer

Multiplayer
This commit is contained in:
griffi-gh 2023-03-09 00:33:19 +01:00 committed by GitHub
commit 8cd8cf35a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
96 changed files with 5349 additions and 896 deletions

4
.gitignore vendored
View file

@ -3,10 +3,6 @@
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

9
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,9 @@
{
"editor.tabSize": 2,
"rust-analyzer.diagnostics.disabled": [
//rust-analyzer issue #14269,
"unresolved-method",
"unresolved-import",
"unresolved-field"
]
}

2317
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,10 @@
[package]
name = "kubi"
version = "0.1.0"
edition = "2021"
[workspace]
members = ["kubi", "kubi-server", "kubi-shared", "kubi-logging"]
resolver = "2"
[dependencies]
glium = "0.32"
image = { version = "0.24", default_features = false, features = ["png"] }
log = "0.4"
env_logger = "0.10"
strum = { version = "0.24", features = ["derive"] }
glam = { version = "0.22", features = ["debug-glam-assert", "mint", "fast-math"] }
hashbrown = "0.13"
rayon = "1.6"
shipyard = { version = "0.6", features = ["thread_local"] }
nohash-hasher = "0.2.0"
anyhow = "1.0"
flume = "0.10"
bracket-noise = "0.8"
#rkyv = "0.7"
[profile.release-with-debug]
inherits = "release"
debug = true
[profile.dev]
opt-level = 1

7
Server.toml Normal file
View file

@ -0,0 +1,7 @@
[server]
address = "0.0.0.0:12345"
max_clients = 254
timeout_ms = 10000
[world]
seed = 0xfeb_face_dead_cafe

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

BIN
assets/blocks/planks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

View file

@ -1 +0,0 @@
sorry no crabs here

8
kubi-logging/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "kubi-logging"
version = "0.1.0"
edition = "2021"
[dependencies]
log = "0.4"
env_logger = "0.10"

View file

@ -1,14 +1,16 @@
//! Custom env_logger options and styling
/// Custom env_logger options and styling
use env_logger::{fmt::Color, Builder, Env};
use log::Level;
use std::io::Write;
pub use log;
pub use env_logger;
#[inline]
pub fn init() {
let mut env = Env::default();
if cfg!(debug_assertions) {
env = env.filter_or("RUST_LOG", "trace");
}
let env = Env::default()
.filter_or("RUST_LOG", "trace,gilrs=warn,rusty_xinput=warn");
Builder::from_env(env)
.format(|buf, record| {
let mut level_style = buf.style();
@ -18,6 +20,9 @@ pub fn init() {
_ => Color::Blue
}).set_bold(true);
let mut bold_style = buf.style();
bold_style.set_bold(true);
let mut location_style = buf.style();
location_style.set_bold(true);
location_style.set_dimmed(true);
@ -25,9 +30,11 @@ pub fn init() {
let mut location_line_style = buf.style();
location_line_style.set_dimmed(true);
let text = format!("{}", record.args());
writeln!(
buf,
"{} {:<50}\t{}{}{}",
"{} {:<50}\t{}{}{}{}",
level_style.value(match record.level() {
Level::Error => "[e]",
Level::Warn => "[w]",
@ -35,7 +42,8 @@ pub fn init() {
Level::Debug => "[d]",
Level::Trace => "[t]",
}),
format!("{}", record.args()),
text,
bold_style.value((text.len() > 50).then_some("\n ╰─ ").unwrap_or_default()),
location_style.value(record.target()),
location_line_style.value(" :"),
location_line_style.value(record.line().unwrap_or(0))

26
kubi-server/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "kubi-server"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
kubi-shared = { path = "../kubi-shared" }
kubi-logging = { path = "../kubi-logging" }
log = "*"
shipyard = { git = "https://github.com/leudz/shipyard", rev = "eb189f66", features = ["thread_local"] }
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
toml = "0.7"
glam = { version = "0.23", features = ["debug-glam-assert", "fast-math"] }
hashbrown = "0.13"
nohash-hasher = "0.2.0"
anyhow = "1.0"
rayon = "1.6"
flume = "0.10"
rand = "0.8"
uflow = "0.7"
postcard = { version = "1.0", features = ["alloc"] }
[features]
default = []
nightly = ["rand/nightly", "rand/simd_support", "serde/unstable", "glam/core-simd", "kubi-shared/nightly"]

86
kubi-server/src/auth.rs Normal file
View file

@ -0,0 +1,86 @@
use shipyard::{UniqueView, NonSendSync};
use uflow::{server::Event as ServerEvent, SendMode};
use kubi_shared::networking::messages::{
ClientToServerMessage,
ServerToClientMessage,
InitData,
C_CLIENT_HELLO
};
use crate::{
server::{ServerEvents, UdpServer, IsMessageOfType},
config::ConfigTable
};
pub fn authenticate_players(
server: NonSendSync<UniqueView<UdpServer>>,
events: UniqueView<ServerEvents>,
config: UniqueView<ConfigTable>
) {
for event in &events.0 {
// if let ServerEvent::MessageReceived {
// from,
// message: ClientToServerMessage::ClientHello {
// username,
// password
// }
// } = event {
let ServerEvent::Receive(client_addr, data) = event else{
continue
};
let Some(client) = server.0.client(client_addr) else {
log::error!("Client doesn't exist");
continue
};
if !event.is_message_of_type::<C_CLIENT_HELLO>() {
continue
}
let Ok(parsed_message) = postcard::from_bytes(data) else {
log::error!("Malformed message");
continue
};
let ClientToServerMessage::ClientHello { username, password } = parsed_message else {
unreachable!()
};
log::info!("ClientHello; username={} password={:?}", username, password);
// Handle password auth
if let Some(server_password) = &config.server.password {
if let Some(user_password) = &password {
if server_password != user_password {
let res = postcard::to_allocvec(&ServerToClientMessage::ServerFuckOff {
reason: "Passwords don't match".into()
}).unwrap().into_boxed_slice();
client.borrow_mut().send(
res, 0, SendMode::Reliable
);
continue
}
} else {
let res = postcard::to_allocvec(&ServerToClientMessage::ServerFuckOff {
reason: "This server is password protected".into()
}).unwrap().into_boxed_slice();
client.borrow_mut().send(
res, 0, SendMode::Reliable
);
continue
}
}
//Spawn the user
//TODO Spawn the user on server side
//Approve the user
let res = postcard::to_allocvec(&ServerToClientMessage::ServerHello {
init: InitData {
users: vec![] //TODO create init data
}
}).unwrap().into_boxed_slice();
client.borrow_mut().send(
res, 0, SendMode::Reliable
);
log::info!("{username} joined the game!")
}
}

19
kubi-server/src/client.rs Normal file
View file

@ -0,0 +1,19 @@
use shipyard::{Component, EntityId};
use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher;
use kubi_shared::networking::client::ClientId;
#[derive(Component)]
pub struct Client(ClientId);
pub struct ClientMap(HashMap<ClientId, EntityId, BuildNoHashHasher<ClientId>>);
impl ClientMap {
pub fn new() -> Self {
Self(HashMap::with_hasher(BuildNoHashHasher::default()))
}
}
impl Default for ClientMap {
fn default() -> Self {
Self::new()
}
}

31
kubi-server/src/config.rs Normal file
View file

@ -0,0 +1,31 @@
use shipyard::{AllStoragesView, Unique};
use serde::{Serialize, Deserialize};
use std::{fs, net::SocketAddr};
#[derive(Serialize, Deserialize)]
pub struct ConfigTableServer {
pub address: SocketAddr,
pub max_clients: usize,
pub timeout_ms: u64,
pub password: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct ConfigTableWorld {
pub seed: u64,
}
#[derive(Unique, Serialize, Deserialize)]
pub struct ConfigTable {
pub server: ConfigTableServer,
pub world: ConfigTableWorld,
}
pub fn read_config(
storages: AllStoragesView,
) {
log::info!("Reading config...");
let config_str = fs::read_to_string("Server.toml").expect("No config file found");
let config: ConfigTable = toml::from_str(&config_str).expect("Invalid configuration file");
storages.add_unique(config);
}

46
kubi-server/src/main.rs Normal file
View file

@ -0,0 +1,46 @@
use shipyard::{World, Workload, IntoWorkload};
use std::{thread, time::Duration};
pub(crate) mod util;
pub(crate) mod config;
pub(crate) mod server;
pub(crate) mod client;
//pub(crate) mod world;
pub(crate) mod auth;
use config::read_config;
use server::{bind_server, update_server, log_server_errors};
use auth::authenticate_players;
//use world::{update_world, init_world};
fn initialize() -> Workload {
(
read_config,
bind_server,
//init_world,
).into_workload()
}
fn update() -> Workload {
(
update_server,
(
log_server_errors,
authenticate_players,
//update_world,
).into_workload()
).into_sequential_workload()
}
fn main() {
kubi_logging::init();
let world = World::new();
world.add_workload(initialize);
world.add_workload(update);
world.run_workload(initialize).unwrap();
log::info!("The server is now running");
loop {
world.run_workload(update).unwrap();
thread::sleep(Duration::from_millis(16));
}
}

62
kubi-server/src/server.rs Normal file
View file

@ -0,0 +1,62 @@
use shipyard::{AllStoragesView, Unique, UniqueView, UniqueViewMut, NonSendSync};
use uflow::{server::{Server, Event as ServerEvent, Config as ServerConfig}, EndpointConfig};
use crate::config::ConfigTable;
#[derive(Unique)]
#[repr(transparent)]
pub struct UdpServer(pub Server);
#[derive(Unique, Default)]
pub struct ServerEvents(pub Vec<ServerEvent>);
pub trait IsMessageOfType {
///Checks if postcard-encoded message has a type
fn is_message_of_type<const T: u8>(&self) -> bool;
}
impl IsMessageOfType for ServerEvent {
fn is_message_of_type<const T: u8>(&self) -> bool {
let ServerEvent::Receive(_, data) = &self else { return false };
if data.len() == 0 { return false }
data[0] == T
}
}
pub fn bind_server(
storages: AllStoragesView,
) {
log::info!("Creating server...");
let config = storages.borrow::<UniqueView<ConfigTable>>().unwrap();
let server = Server::bind(
config.server.address,
ServerConfig {
max_total_connections: config.server.max_clients,
max_active_connections: config.server.max_clients,
enable_handshake_errors: true,
endpoint_config: EndpointConfig {
active_timeout_ms: config.server.timeout_ms,
..Default::default()
},
..Default::default()
}
).expect("Failed to create the server");
storages.add_unique_non_send_sync(UdpServer(server));
storages.add_unique(ServerEvents::default());
}
pub fn update_server(
mut server: NonSendSync<UniqueViewMut<UdpServer>>,
mut events: UniqueViewMut<ServerEvents>,
) {
events.0.clear();
events.0.extend(server.0.step());
}
pub fn log_server_errors(
events: UniqueView<ServerEvents>,
) {
for event in &events.0 {
if let ServerEvent::Error(addr, error) = event {
log::error!("Server error addr: {addr} error: {error:?}");
}
}
}

3
kubi-server/src/util.rs Normal file
View file

@ -0,0 +1,3 @@
pub fn log_error(error: anyhow::Error) {
log::error!("{}", error);
}

119
kubi-server/src/world.rs Normal file
View file

@ -0,0 +1,119 @@
use shipyard::{Unique, UniqueView, UniqueViewMut, Workload, IntoWorkload, AllStoragesView};
use glam::IVec3;
use hashbrown::HashMap;
use kubi_shared::networking::messages::{ClientToServerMessage, ServerToClientMessage};
use crate::{
server::{UdpServer, ServerEvents},
config::ConfigTable,
util::log_error,
};
pub mod chunk;
pub mod tasks;
use chunk::Chunk;
use self::{tasks::{ChunkTaskManager, ChunkTask, ChunkTaskResponse, init_chunk_task_manager}, chunk::ChunkState};
#[derive(Unique, Default)]
pub struct ChunkManager {
pub chunks: HashMap<IVec3, Chunk>
}
impl ChunkManager {
pub fn new() -> Self {
Self::default()
}
}
fn process_chunk_requests(
mut server: UniqueViewMut<UdpServer>,
events: UniqueView<ServerEvents>,
mut chunk_manager: UniqueViewMut<ChunkManager>,
task_manager: UniqueView<ChunkTaskManager>,
config: UniqueView<ConfigTable>
) {
for event in &events.0 {
if let ServerEvent::MessageReceived {
from: client_id,
message: ClientToServerMessage::ChunkSubRequest {
chunk: chunk_position
}
} = event {
let chunk_position = IVec3::from_array(*chunk_position);
if let Some(chunk) = chunk_manager.chunks.get_mut(&chunk_position) {
chunk.subscriptions.insert(*client_id);
//TODO Start task here if status is "Nothing"
if let Some(blocks) = &chunk.blocks {
server.0.send_message(*client_id, kubi_shared::networking::messages::ServerToClientMessage::ChunkResponse {
chunk: chunk_position.to_array(),
data: blocks.clone(),
queued: Vec::with_capacity(0)
}).map_err(log_error).ok();
}
} else {
let mut chunk = Chunk::new(chunk_position);
chunk.state = ChunkState::Loading;
chunk.subscriptions.insert(*client_id);
chunk_manager.chunks.insert(chunk_position, chunk);
task_manager.spawn_task(ChunkTask::LoadChunk {
position: chunk_position,
seed: config.world.seed,
});
}
}
}
}
fn process_finished_tasks(
mut server: UniqueViewMut<UdpServer>,
task_manager: UniqueView<ChunkTaskManager>,
mut chunk_manager: UniqueViewMut<ChunkManager>,
) {
let mut limit: usize = 8;
while let Some(res) = task_manager.receive() {
let ChunkTaskResponse::ChunkLoaded { chunk_position, blocks, queue } = res;
let Some(chunk) = chunk_manager.chunks.get_mut(&chunk_position) else {
log::warn!("Chunk discarded: Doesn't exist");
continue
};
if chunk.state != ChunkState::Loading {
log::warn!("Chunk discarded: Not Loading");
continue
}
chunk.state = ChunkState::Loaded;
chunk.blocks = Some(blocks.clone());
for &subscriber in &chunk.subscriptions {
server.0.send_message(subscriber, ServerToClientMessage::ChunkResponse {
chunk: chunk_position.to_array(),
data: blocks.clone(),
queued: queue
}).map_err(log_error).ok();
}
log::debug!("Chunk {chunk_position} loaded, {} subs", chunk.subscriptions.len());
//HACK: Implement proper flow control/reliable transport in kubi-udp
limit -= 1;
if limit == 0 {
break;
}
}
}
fn init_chunk_manager(
storages: AllStoragesView
) {
storages.add_unique(ChunkManager::new());
}
pub fn init_world() -> Workload {
(
init_chunk_manager,
init_chunk_task_manager,
).into_workload()
}
pub fn update_world() -> Workload {
(
process_chunk_requests,
process_finished_tasks,
).into_workload()
}

View file

@ -0,0 +1,31 @@
use glam::IVec3;
use hashbrown::HashSet;
use nohash_hasher::BuildNoHashHasher;
use kubi_shared::{
chunk::BlockData,
networking::client::ClientId
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChunkState {
Nothing,
Loading,
Loaded,
}
pub struct Chunk {
pub position: IVec3,
pub state: ChunkState,
pub blocks: Option<BlockData>,
pub subscriptions: HashSet<ClientId, BuildNoHashHasher<ClientId>>,
}
impl Chunk {
pub fn new(position: IVec3) -> Self {
Self {
position,
state: ChunkState::Nothing,
blocks: None,
subscriptions: HashSet::with_hasher(BuildNoHashHasher::default()),
}
}
}

View file

@ -0,0 +1,59 @@
use shipyard::{Unique, AllStoragesView};
use flume::{unbounded, Sender, Receiver};
use glam::IVec3;
use rayon::{ThreadPool, ThreadPoolBuilder};
use anyhow::Result;
use kubi_shared::{
chunk::BlockData,
worldgen::generate_world,
queue::QueuedBlock,
};
pub enum ChunkTask {
LoadChunk {
position: IVec3,
seed: u64,
}
}
pub enum ChunkTaskResponse {
ChunkLoaded {
chunk_position: IVec3,
blocks: BlockData,
queue: Vec<QueuedBlock>
}
}
#[derive(Unique)]
pub struct ChunkTaskManager {
channel: (Sender<ChunkTaskResponse>, Receiver<ChunkTaskResponse>),
pool: ThreadPool,
}
impl ChunkTaskManager {
pub fn new() -> Result<Self> {
Ok(Self {
channel: unbounded(),
pool: ThreadPoolBuilder::new().build()?
})
}
pub fn spawn_task(&self, task: ChunkTask) {
let sender = self.channel.0.clone();
self.pool.spawn(move || {
sender.send(match task {
ChunkTask::LoadChunk { position: chunk_position, seed } => {
let (blocks, queue) = generate_world(chunk_position, seed);
ChunkTaskResponse::ChunkLoaded { chunk_position, blocks, queue }
}
}).unwrap()
})
}
pub fn receive(&self) -> Option<ChunkTaskResponse> {
self.channel.1.try_recv().ok()
}
}
pub fn init_chunk_task_manager(
storages: AllStoragesView
) {
storages.add_unique(ChunkTaskManager::new().expect("ChunkTaskManager Init failed"));
}

22
kubi-shared/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "kubi-shared"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
glam = { version = "0.23", features = ["debug-glam-assert", "fast-math", "serde"] }
shipyard = { git = "https://github.com/leudz/shipyard", rev = "eb189f66", default-features = false, features = ["std"] }
strum = { version = "0.24", features = ["derive"] }
postcard = { version = "1.0", features = ["alloc"] }
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
anyhow = "1.0"
bracket-noise = "0.8"
rand = { version = "0.8", default_features = false, features = ["std", "min_const_gen"] }
rand_xoshiro = "0.6"
[features]
default = []
nightly = ["rand/nightly", "rand/simd_support", "serde/unstable", "glam/core-simd"]

211
kubi-shared/src/block.rs Normal file
View file

@ -0,0 +1,211 @@
use serde::{Serialize, Deserialize};
use strum::EnumIter;
#[derive(Serialize, Deserialize, Clone, Copy, Debug, EnumIter)]
#[repr(u8)]
pub enum BlockTexture {
Stone,
Dirt,
GrassTop,
GrassSide,
Sand,
Bedrock,
Wood,
WoodTop,
Leaf,
Torch,
TallGrass,
Snow,
GrassSideSnow,
Cobblestone,
Planks,
WaterSolid,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, EnumIter)]
#[repr(u8)]
pub enum Block {
Air,
Marker,
Stone,
Dirt,
Grass,
Sand,
Cobblestone,
TallGrass,
Planks,
Torch,
Wood,
Leaf,
Water,
}
impl Block {
#[inline]
pub const fn descriptor(self) -> BlockDescriptor {
match self {
Self::Air => BlockDescriptor {
name: "air",
render: RenderType::None,
collision: CollisionType::None,
raycast_collision: false,
},
Self::Marker => BlockDescriptor {
name: "marker",
render: RenderType::None,
collision: CollisionType::None,
raycast_collision: false,
},
Self::Stone => BlockDescriptor {
name: "stone",
render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Stone)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::Dirt => BlockDescriptor {
name: "dirt",
render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Dirt)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::Grass => BlockDescriptor {
name: "grass",
render: RenderType::SolidBlock(CubeTexture::top_sides_bottom(
BlockTexture::GrassTop,
BlockTexture::GrassSide,
BlockTexture::Dirt
)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::Sand => BlockDescriptor {
name: "sand",
render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Sand)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::Cobblestone => BlockDescriptor {
name: "cobblestone",
render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Cobblestone)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::TallGrass => BlockDescriptor {
name: "tall grass",
render: RenderType::CrossShape(CrossTexture::all(BlockTexture::TallGrass)),
collision: CollisionType::None,
raycast_collision: true,
},
Self::Planks => BlockDescriptor {
name: "planks",
render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Planks)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::Torch => BlockDescriptor {
name: "torch",
render: RenderType::CrossShape(CrossTexture::all(BlockTexture::Torch)),
collision: CollisionType::None,
raycast_collision: true,
},
Self::Wood => BlockDescriptor {
name: "leaf",
render: RenderType::SolidBlock(CubeTexture::horizontal_vertical(BlockTexture::Wood, BlockTexture::WoodTop)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::Leaf => BlockDescriptor {
name: "leaf",
render: RenderType::BinaryTransparency(CubeTexture::all(BlockTexture::Leaf)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::Water => BlockDescriptor {
name: "water",
render: RenderType::BinaryTransparency(CubeTexture::all(BlockTexture::WaterSolid)),
collision: CollisionType::None,
raycast_collision: true,
},
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct BlockDescriptor {
pub name: &'static str,
pub render: RenderType,
pub collision: CollisionType,
pub raycast_collision: bool,
}
// impl BlockDescriptor {
// pub fn of(block: Block) -> Self {
// block.descriptor()
// }
// }
#[derive(Clone, Copy, Debug)]
pub struct CubeTexture {
pub top: BlockTexture,
pub bottom: BlockTexture,
pub left: BlockTexture,
pub right: BlockTexture,
pub front: BlockTexture,
pub back: BlockTexture,
}
impl CubeTexture {
pub const fn top_sides_bottom(top: BlockTexture, sides: BlockTexture, bottom: BlockTexture) -> Self {
Self {
top,
bottom,
left: sides,
right: sides,
front: sides,
back: sides,
}
}
pub const fn horizontal_vertical(horizontal: BlockTexture, vertical: BlockTexture) -> Self {
Self::top_sides_bottom(vertical, horizontal, vertical)
}
pub const fn all(texture: BlockTexture) -> Self {
Self::horizontal_vertical(texture, texture)
}
}
#[derive(Clone, Copy, Debug)]
pub struct CrossTextureSides {
pub front: BlockTexture,
pub back: BlockTexture
}
impl CrossTextureSides {
pub const fn all(texture: BlockTexture) -> Self {
Self {
front: texture,
back: texture
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct CrossTexture(pub CrossTextureSides, pub CrossTextureSides);
impl CrossTexture {
pub const fn all(texture: BlockTexture) -> Self {
Self(
CrossTextureSides::all(texture),
CrossTextureSides::all(texture)
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CollisionType {
None,
Solid,
}
#[derive(Clone, Copy, Debug)]
pub enum RenderType {
None,
SolidBlock(CubeTexture),
BinaryTransparency(CubeTexture),
CrossShape(CrossTexture),
}

4
kubi-shared/src/chunk.rs Normal file
View file

@ -0,0 +1,4 @@
use crate::block::Block;
pub const CHUNK_SIZE: usize = 32;
pub type BlockData = Box<[[[Block; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]>;

18
kubi-shared/src/entity.rs Normal file
View file

@ -0,0 +1,18 @@
use shipyard::Component;
#[derive(Component)]
pub struct Entity;
#[derive(Component)]
pub struct Health {
pub current: u8,
pub max: u8,
}
impl Health {
fn new(health: u8) -> Self {
Self {
current: health,
max: health
}
}
}

8
kubi-shared/src/lib.rs Normal file
View file

@ -0,0 +1,8 @@
pub mod block;
pub mod networking;
pub mod worldgen;
pub mod chunk;
pub mod transform;
pub mod entity;
pub mod player;
pub mod queue;

View file

@ -0,0 +1,3 @@
pub mod messages;
pub mod state;
pub mod client;

View file

@ -0,0 +1,3 @@
pub type ClientId = u16;
pub type ClientKey = u16;

View file

@ -0,0 +1,72 @@
use std::num::NonZeroUsize;
use serde::{Serialize, Deserialize};
use crate::{chunk::BlockData, queue::QueuedBlock};
pub type IVec3Arr = [i32; 3];
pub type Vec3Arr = [f32; 3];
pub type QuatArr = [f32; 3];
pub const PROTOCOL_ID: u16 = 2;
pub const C_CLIENT_HELLO: u8 = 0;
pub const C_POSITION_CHANGED: u8 = 1;
pub const C_CHUNK_SUB_REQUEST: u8 = 2;
#[derive(Serialize, Deserialize, Clone)]
#[repr(u8)]
pub enum ClientToServerMessage {
ClientHello {
username: String,
password: Option<String>,
} = C_CLIENT_HELLO,
PositionChanged {
position: Vec3Arr,
velocity: Vec3Arr,
direction: QuatArr,
} = C_POSITION_CHANGED,
ChunkSubRequest {
chunk: IVec3Arr,
} = C_CHUNK_SUB_REQUEST,
}
pub const S_SERVER_HELLO: u8 = 0;
pub const S_SERVER_FUCK_OFF: u8 = 1;
pub const S_PLAYER_POSITION_CHANGED: u8 = 2;
pub const S_CHUNK_RESPONSE: u8 = 3;
#[derive(Serialize, Deserialize, Clone)]
#[repr(u8)]
pub enum ServerToClientMessage {
ServerHello {
init: InitData
} = S_SERVER_HELLO,
ServerFuckOff {
reason: String,
} = S_SERVER_FUCK_OFF,
PlayerPositionChanged {
client_id: u8,
position: Vec3Arr,
direction: QuatArr,
} = S_PLAYER_POSITION_CHANGED,
ChunkResponse {
chunk: IVec3Arr,
data: BlockData,
queued: Vec<QueuedBlock>,
} = S_CHUNK_RESPONSE,
}
//---
#[derive(Serialize, Deserialize, Clone)]
pub struct UserInitData {
pub client_id: NonZeroUsize, //maybe use the proper type instead
pub username: String,
pub position: Vec3Arr,
pub velocity: Vec3Arr,
pub direction: QuatArr,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct InitData {
pub users: Vec<UserInitData>
}

View file

@ -0,0 +1,15 @@
use shipyard::{Unique, Component};
// disconnected => connect => join => load => ingame
#[derive(Unique, Component, PartialEq, Eq, Clone, Copy, Debug)]
#[repr(u8)]
pub enum ClientJoinState {
/// Not connected yet
Disconnected,
/// Client has connected to the game, but hasn't authenticated yet
Connected,
/// Client has joined the game, but hasn't loaded the world yet
Joined,
/// Client is currently ingame
InGame,
}

View file

@ -0,0 +1,4 @@
use shipyard::Component;
#[derive(Component)]
pub struct Player;

11
kubi-shared/src/queue.rs Normal file
View file

@ -0,0 +1,11 @@
use glam::IVec3;
use serde::{Serialize, Deserialize};
use crate::block::Block;
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub struct QueuedBlock {
pub position: IVec3,
pub block_type: Block,
/// Only replace air blocks
pub soft: bool,
}

View file

@ -0,0 +1,8 @@
use shipyard::Component;
use glam::{Mat4, Mat3};
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct Transform(pub Mat4);
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct Transform2d(pub Mat3);

333
kubi-shared/src/worldgen.rs Normal file
View file

@ -0,0 +1,333 @@
use bracket_noise::prelude::*;
use rand::prelude::*;
use glam::{IVec3, ivec3, Vec3Swizzles, IVec2};
use rand_xoshiro::Xoshiro256StarStar;
use crate::{
chunk::{BlockData, CHUNK_SIZE},
block::Block,
queue::QueuedBlock,
};
fn mountain_ramp(mut x: f32) -> f32 {
x = x * 2.0;
if x < 0.4 {
0.5 * x
} else if x < 0.55 {
4. * (x - 0.4) + 0.2
} else {
0.4444 * (x - 0.55) + 0.8
}
}
fn local_height(height: i32, chunk_position: IVec3) -> usize {
let offset = chunk_position * CHUNK_SIZE as i32;
(height - offset.y).clamp(0, CHUNK_SIZE as i32) as usize
}
fn local_y_position(height: i32, chunk_position: IVec3) -> Option<usize> {
let offset = chunk_position * CHUNK_SIZE as i32;
let position = height - offset.y;
(0..CHUNK_SIZE as i32).contains(&position).then_some(position as usize)
}
pub fn generate_world(chunk_position: IVec3, seed: u64) -> (BlockData, Vec<QueuedBlock>) {
let offset = chunk_position * CHUNK_SIZE as i32;
let mut blocks = Box::new([[[Block::Air; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]);
let mut queue = Vec::with_capacity(0);
let mut smart_place = |blocks: &mut BlockData, position: IVec3, block: Block| {
if position.to_array().iter().any(|&x| !(0..CHUNK_SIZE).contains(&(x as usize))) {
let event_pos = offset + position;
queue.retain(|block: &QueuedBlock| {
block.position != event_pos
});
queue.push(QueuedBlock {
position: event_pos,
block_type: block,
soft: true
});
} else {
blocks[position.x as usize][position.y as usize][position.z as usize] = block;
}
};
let mut height_noise = FastNoise::seeded(seed);
height_noise.set_fractal_type(FractalType::FBM);
height_noise.set_fractal_octaves(4);
height_noise.set_frequency(0.003);
let mut elevation_noise = FastNoise::seeded(seed.rotate_left(1));
elevation_noise.set_fractal_type(FractalType::FBM);
elevation_noise.set_fractal_octaves(1);
elevation_noise.set_frequency(0.001);
let mut cave_noise_a = FastNoise::seeded(seed.rotate_left(2));
cave_noise_a.set_fractal_type(FractalType::FBM);
cave_noise_a.set_fractal_octaves(2);
cave_noise_a.set_frequency(0.01);
let mut cave_noise_b = FastNoise::seeded(seed.rotate_left(3));
cave_noise_b.set_fractal_type(FractalType::FBM);
cave_noise_b.set_fractal_octaves(3);
cave_noise_b.set_frequency(0.015);
let mut cave_noise_holes = FastNoise::seeded(seed.rotate_left(4));
cave_noise_holes.set_fractal_type(FractalType::FBM);
cave_noise_holes.set_fractal_octaves(2);
cave_noise_holes.set_frequency(0.005);
let mut ravine_nose_line = FastNoise::seeded(seed.rotate_left(5));
ravine_nose_line.set_fractal_type(FractalType::Billow);
ravine_nose_line.set_fractal_octaves(2);
ravine_nose_line.set_frequency(0.005);
let mut ravine_noise_location = FastNoise::seeded(seed.rotate_left(6));
ravine_noise_location.set_fractal_type(FractalType::FBM);
ravine_noise_location.set_fractal_octaves(1);
ravine_noise_location.set_frequency(0.005);
let mut river_noise = FastNoise::seeded(seed.rotate_left(7));
river_noise.set_fractal_type(FractalType::Billow);
river_noise.set_fractal_octaves(2);
river_noise.set_frequency(0.5 * 0.005);
let mut rng = Xoshiro256StarStar::seed_from_u64(
seed
^ ((chunk_position.x as u32 as u64) << 0)
^ ((chunk_position.z as u32 as u64) << 32)
);
let rng_map_a: [[f32; CHUNK_SIZE]; CHUNK_SIZE] = rng.gen();
let rng_map_b: [[f32; CHUNK_SIZE]; CHUNK_SIZE] = rng.gen();
//Generate height map
let mut within_heightmap = false;
let mut deco_heightmap = [[None; CHUNK_SIZE]; CHUNK_SIZE];
for x in 0..CHUNK_SIZE {
for z in 0..CHUNK_SIZE {
let (noise_x, noise_y) = ((offset.x + x as i32) as f32, (offset.z + z as i32) as f32);
//sample noises (that are needed right now)
let raw_heightmap_value = height_noise.get_noise(noise_x, noise_y);
let raw_elevation_value = elevation_noise.get_noise(noise_x, noise_y);
let raw_ravine_location_value = ravine_noise_location.get_noise(noise_x, noise_y);
//compute height
let mut is_surface = true;
let mut river_fill_height = None;
let height = {
let local_elevation = raw_elevation_value.powi(4).sqrt();
let mut height = (mountain_ramp(raw_heightmap_value) * local_elevation * 100.) as i32;
//Flatten valleys
if height < 0 {
height /= 2;
}
//Generate rivers
{
let river_width = (height as f32 / -5.).clamp(0.5, 1.);
let river_value = river_noise.get_noise(noise_x, noise_y);
if ((-0.00625 * river_width)..(0.00625 * river_width)).contains(&(river_value.powi(2))) {
is_surface = false;
river_fill_height = Some(height - 1);
//river_fill_height = Some(-3);
height -= (river_width * 15. * ((0.00625 * river_width) - river_value.powi(2)) * (1. / (0.00625 * river_width))).round() as i32;
}
}
//Generate ravines
if height < 0 {
if raw_ravine_location_value > 0.4 {
let raw_ravine_value = ravine_nose_line.get_noise(noise_x, noise_y);
if (-0.0125..0.0125).contains(&(raw_ravine_value.powi(2))) {
is_surface = false;
height -= (100. * (0.0125 - raw_ravine_value.powi(2)) * (1. / 0.0125)).round() as i32;
}
}
}
height
};
//add to heightmap
if is_surface {
deco_heightmap[x as usize][z as usize] = Some(height);
//place dirt
for y in 0..local_height(height, chunk_position) {
blocks[x][y][z] = Block::Dirt;
within_heightmap = true;
}
//place stone
for y in 0..local_height(height - 5 - (raw_heightmap_value * 5.) as i32, chunk_position) {
blocks[x][y][z] = Block::Stone;
within_heightmap = true;
}
//place grass
if let Some(y) = local_y_position(height, chunk_position) {
blocks[x][y][z] = Block::Grass;
within_heightmap = true;
}
} else {
if let Some(river_fill_height) = river_fill_height {
//Place water
for y in 0..local_height(river_fill_height, chunk_position) {
blocks[x][y][z] = Block::Water;
within_heightmap = true;
}
//Place stone
for y in 0..local_height(height - 1, chunk_position) {
blocks[x][y][z] = Block::Stone;
within_heightmap = true;
}
//Place dirt
if let Some(y) = local_y_position(height, chunk_position) {
blocks[x][y][z] = Block::Dirt;
within_heightmap = true;
}
} else {
//Place stone
for y in 0..local_height(height, chunk_position) {
blocks[x][y][z] = Block::Stone;
within_heightmap = true;
}
}
}
}
}
//Carve out caves
if within_heightmap {
for z in 0..CHUNK_SIZE {
for y in 0..CHUNK_SIZE {
for x in 0..CHUNK_SIZE {
if blocks[x][y][z] != Block::Stone { continue }
let cave_size = ((offset.y + y as i32) as f32 / -100.).clamp(0., 1.);
let inv_cave_size = 1. - cave_size;
if cave_size < 0.1 { continue }
let position = ivec3(x as i32, y as i32, z as i32) + offset;
let is_cave = || {
let raw_cavemap_value_a = cave_noise_a.get_noise3d(position.x as f32, position.y as f32, position.z as f32);
let raw_cavemap_value_b = cave_noise_b.get_noise3d(position.x as f32, position.y as f32, position.z as f32);
((cave_size * -0.15)..=(cave_size * 0.15)).contains(&raw_cavemap_value_a) &&
((cave_size * -0.15)..=(cave_size * 0.15)).contains(&raw_cavemap_value_b)
};
let is_hole_cave = || {
let raw_cavemap_value_holes = cave_noise_holes.get_noise3d(position.x as f32, position.y as f32, position.z as f32);
((0.9 + (0.1 * inv_cave_size))..=1.0).contains(&raw_cavemap_value_holes.abs())
};
if is_cave() || is_hole_cave() {
blocks[x][y][z] = Block::Air;
if deco_heightmap[x][z] == Some(y as i32 + offset.y) {
deco_heightmap[x][z] = None
}
}
}
}
}
}
//Add decorations
for x in 0..CHUNK_SIZE {
for z in 0..CHUNK_SIZE {
//get height
let Some(height) = deco_heightmap[x][z] else { continue };
//check for air
// if blocks[x][local_y][z] == Block::Air {
// continue
// }
//place tall grass
if rng_map_a[x][z] < 0.03 {
if let Some(y) = local_y_position(height + 1, chunk_position) {
blocks[x][y][z] = Block::TallGrass;
}
}
//place trees!
if rng_map_a[x][z] < 0.001 {
//Place wood (no smart_place needed here!)
let tree_height = 4 + (rng_map_b[x][z] * 3.).round() as i32;
for tree_y in 0..tree_height {
if let Some(y) = local_y_position(height + 1 + tree_y, chunk_position) {
blocks[x][y][z] = Block::Wood;
}
}
let tree_height = 4 + (rng_map_b[x][z] * 3.).round() as i32;
//Place leaf blocks
if let Some(y) = local_y_position(height + 1, chunk_position) {
let tree_pos = ivec3(x as i32, y as i32, z as i32);
// Place wood (smart_place)
// for tree_y in 0..tree_height {
// smart_place(&mut blocks, tree_pos + tree_y * IVec3::Y, Block::Wood);
// }
// Part that wraps around the tree
{
let tree_leaf_height = tree_height - 3;
let leaf_width = 2;
for tree_y in tree_leaf_height..tree_height {
for tree_x in (-leaf_width)..=leaf_width {
for tree_z in (-leaf_width)..=leaf_width {
let tree_offset = ivec3(tree_x, tree_y, tree_z);
if tree_offset.xz() == IVec2::ZERO { continue }
smart_place(&mut blocks, tree_pos + tree_offset, Block::Leaf);
}
}
}
}
//part above the tree
{
let leaf_above_height = 2;
let leaf_width = 1;
for tree_y in tree_height..(tree_height + leaf_above_height) {
for tree_x in (-leaf_width)..=leaf_width {
for tree_z in (-leaf_width)..=leaf_width {
let tree_offset = ivec3(tree_x, tree_y, tree_z);
smart_place(&mut blocks, tree_pos + tree_offset, Block::Leaf);
}
}
}
}
}
}
}
}
(blocks, queue)
// let mut cave_noise = FastNoise::seeded(seed);
// cave_noise.set_fractal_type(FractalType::FBM);
// cave_noise.set_frequency(0.1);
// let mut dirt_noise = FastNoise::seeded(seed.rotate_left(1));
// dirt_noise.set_fractal_type(FractalType::FBM);
// dirt_noise.set_frequency(0.1);
//
// if chunk_position.y >= 0 {
// if chunk_position.y == 0 {
// for x in 0..CHUNK_SIZE {
// for z in 0..CHUNK_SIZE {
// blocks[x][0][z] = Block::Dirt;
// blocks[x][1][z] = Block::Grass;
// }
// }
// }
// } else {
// for x in 0..CHUNK_SIZE {
// for y in 0..CHUNK_SIZE {
// for z in 0..CHUNK_SIZE {
// let position = ivec3(x as i32, y as i32, z as i32) + offset;
// let v_cave_noise = cave_noise.get_noise3d(position.x as f32, position.y as f32, position.z as f32) * (-position.y as f32 - 10.0).clamp(0., 1.);
// let v_dirt_noise = dirt_noise.get_noise3d(position.x as f32, position.y as f32, position.z as f32) * (-position.y as f32).clamp(0., 1.);
// if v_cave_noise > 0.5 {
// blocks[x][y][z] = Block::Stone;
// } else if v_dirt_noise > 0.5 {
// blocks[x][y][z] = Block::Dirt;
// }
// }
// }
// }
// }
// blocks
}

31
kubi/Cargo.toml Normal file
View file

@ -0,0 +1,31 @@
[package]
name = "kubi"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
kubi-shared = { path = "../kubi-shared" }
kubi-logging = { path = "../kubi-logging" }
log = "*"
glium = "0.32"
glam = { version = "0.23", features = ["debug-glam-assert", "fast-math"] }
image = { version = "0.24", default_features = false, features = ["png"] }
strum = { version = "0.24", features = ["derive"] }
hashbrown = "0.13"
rayon = "1.6"
shipyard = { git = "https://github.com/leudz/shipyard", rev = "eb189f66", default-features = false, features = ["std", "proc", "thread_local"] }
nohash-hasher = "0.2.0"
anyhow = "1.0"
flume = "0.10"
gilrs = { version = "0.10", default_features = false, features = ["xinput"] }
uflow = "0.7"
postcard = { version = "1.0", features = ["alloc"] }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3" }
[features]
default = []
parallel = ["shipyard/parallel"]
nightly = ["glam/core-simd", "kubi-shared/nightly"]

View file

@ -1,4 +1,6 @@
#version 150 core
#version 300 es
precision highp float;
out vec4 out_color;
uniform vec4 color;

View file

@ -1,4 +1,6 @@
#version 150 core
#version 300 es
precision highp float;
in vec3 position;
uniform mat4 model;

View file

@ -0,0 +1,17 @@
#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 out_color;
uniform float progress;
uniform vec4 color;
uniform vec4 bg_color;
void main() {
if (v_uv.x <= progress) {
out_color = color;
} else {
out_color = bg_color;
}
}

View file

@ -0,0 +1,12 @@
#version 300 es
in vec2 position;
out vec2 v_uv;
uniform mat4 ui_view;
uniform mat3 transform;
void main() {
v_uv = position;
vec2 transformed = (transform * vec3(position, 1.)).xy;
gl_Position = ui_view * vec4(transformed, 0., 1.);
}

View file

@ -0,0 +1,11 @@
#version 300 es
precision highp float;
out vec4 color;
uniform vec4 u_color;
void main() {
color = u_color;
// color -= vec4(0, 0, 0, 0.1 * sin(gl_FragCoord.x) * cos(gl_FragCoord.y));
}

View file

@ -1,4 +1,6 @@
#version 150 core
#version 300 es
precision highp float;
in vec3 position;
uniform ivec3 u_position;

View file

@ -1,4 +1,7 @@
#version 150 core
#version 300 es
precision highp float;
precision lowp sampler2DArray;
in vec3 v_normal;
in vec2 v_uv;
@ -9,6 +12,10 @@ uniform sampler2DArray tex;
void main() {
// base color from texture
color = texture(tex, vec3(v_uv, v_tex_index));
// discard fully transparent pixels
if (color.w <= 0.0) {
discard;
}
//basic "lighting"
float light = abs(v_normal.x) + .8 * abs(v_normal.y) + .6 * abs(v_normal.z);
color *= vec4(vec3(light), 1.);

View file

@ -1,4 +1,6 @@
#version 150 core
#version 300 es
precision highp float;
//TODO pack this data:
// uint position_normal_uv

View file

@ -0,0 +1,94 @@
use shipyard::{UniqueViewMut, UniqueView, View, IntoIter, ViewMut, EntitiesViewMut, Component, Workload, IntoWorkload};
use glium::glutin::event::VirtualKeyCode;
use kubi_shared::{
block::Block,
queue::QueuedBlock,
};
use crate::{
player::MainPlayer,
world::{raycast::{LookingAtBlock, RAYCAST_STEP}, queue::BlockUpdateQueue},
input::{Inputs, PrevInputs, RawKbmInputState},
events::{EventComponent, player_actions::PlayerActionEvent},
};
#[derive(Component)]
pub struct PlayerHolding(pub Block);
impl Default for PlayerHolding {
fn default() -> Self {
Self(Block::Cobblestone)
}
}
const BLOCK_KEY_MAP: &[(VirtualKeyCode, Block)] = &[
(VirtualKeyCode::Key1, Block::Cobblestone),
(VirtualKeyCode::Key2, Block::Planks),
(VirtualKeyCode::Key3, Block::Dirt),
(VirtualKeyCode::Key4, Block::Grass),
(VirtualKeyCode::Key5, Block::Sand),
(VirtualKeyCode::Key6, Block::Stone),
(VirtualKeyCode::Key7, Block::Torch),
(VirtualKeyCode::Key8, Block::Leaf),
];
fn pick_block_with_number_keys(
main_player: View<MainPlayer>,
mut holding: ViewMut<PlayerHolding>,
input: UniqueView<RawKbmInputState>,
) {
let Some((_, mut holding)) = (&main_player, &mut holding).iter().next() else { return };
for &(key, block) in BLOCK_KEY_MAP {
if input.keyboard_state.contains(&key) {
holding.0 = block;
return
}
}
}
fn block_placement_system(
main_player: View<MainPlayer>,
holding: View<PlayerHolding>,
raycast: View<LookingAtBlock>,
input: UniqueView<Inputs>,
prev_input: UniqueView<PrevInputs>,
mut block_event_queue: UniqueViewMut<BlockUpdateQueue>,
mut entities: EntitiesViewMut,
mut events: ViewMut<EventComponent>,
mut player_events: ViewMut<PlayerActionEvent>,
) {
let action_place = input.action_b && !prev_input.0.action_b;
let action_break = input.action_a && !prev_input.0.action_a;
if action_place ^ action_break {
//get components
let Some((_, ray, block)) = (&main_player, &raycast, &holding).iter().next() else { return };
let Some(ray) = ray.0 else { return };
//get coord and block type
let (place_position, place_block) = if action_place {
if block.0 == Block::Air { return }
let position = (ray.position - ray.direction * (RAYCAST_STEP + 0.001)).floor().as_ivec3();
(position, block.0)
} else {
(ray.block_position, Block::Air)
};
//queue place
block_event_queue.push(QueuedBlock {
position: place_position,
block_type: place_block,
soft: place_block != Block::Air,
});
//send event
entities.add_entity(
(&mut events, &mut player_events),
(EventComponent, PlayerActionEvent::UpdatedBlock {
position: place_position,
block: place_block,
})
);
}
}
pub fn update_block_placement() -> Workload {
(
pick_block_with_number_keys,
block_placement_system
).into_workload()
}

View file

@ -8,7 +8,7 @@
// three layers of stolen code, yay!
use glam::{Vec3A, Vec4, Mat3A, vec3a, Vec3, vec4};
use shipyard::{ViewMut, IntoIter, View};
use shipyard::{ViewMut, IntoIter, View, track};
use crate::transform::Transform;
use super::Camera;
@ -122,9 +122,9 @@ fn intersection<const A: usize, const B: usize, const C: usize>(planes: &[Vec4;
pub fn update_frustum(
mut cameras: ViewMut<Camera>,
transforms: View<Transform>
transforms: View<Transform, { track::All }>
) {
for (camera, _) in (&mut cameras, transforms.inserted_or_modified()).iter() {
camera.frustum = Frustum::compute(camera);
for (mut camera, _) in (&mut cameras, transforms.inserted_or_modified()).iter() {
camera.frustum = Frustum::compute(&camera);
}
}

View file

@ -0,0 +1,48 @@
use glam::{Vec3, Mat4};
use shipyard::{ViewMut, View, IntoIter, Workload, IntoWorkload, track, UniqueView, SystemModificator};
use crate::{transform::Transform, rendering::WindowSize, events::WindowResizedEvent};
use super::Camera;
//maybe parallelize these two?
fn update_view_matrix(
mut vm_camera: ViewMut<Camera>,
v_transform: View<Transform, { track::All }>
) {
for (mut camera, transform) in (&mut vm_camera, v_transform.inserted_or_modified()).iter() {
let (_, rotation, translation) = transform.0.to_scale_rotation_translation();
let direction = (rotation.normalize() * Vec3::NEG_Z).normalize();
camera.view_matrix = Mat4::look_to_rh(translation, direction, camera.up);
}
}
fn update_perspective_matrix(
mut vm_camera: ViewMut<Camera>,
size: UniqueView<WindowSize>,
) {
for mut camera in (&mut vm_camera).iter() {
camera.perspective_matrix = Mat4::perspective_rh_gl(
camera.fov,
size.0.x as f32 / size.0.y as f32,
camera.z_near,
camera.z_far,
)
}
}
fn need_perspective_calc(
v_camera: View<Camera>,
resize_event: View<WindowResizedEvent>,
) -> bool {
(resize_event.len() > 0) ||
(v_camera.iter().any(|camera| {
camera.perspective_matrix == Mat4::default()
}))
}
pub fn update_matrices() -> Workload {
(
update_view_matrix,
update_perspective_matrix.run_if(need_perspective_calc),
).into_workload()
}

12
kubi/src/color.rs Normal file
View file

@ -0,0 +1,12 @@
use glam::{Vec4, vec4};
#[inline(always)]
pub fn color_rgba(r: u8, g: u8, b: u8, a: u8) -> Vec4 {
vec4(r as f32 / 255., g as f32 / 255., b as f32 / 255., a as f32 / 255.)
}
#[inline(always)]
pub fn color_hex(c: u32) -> Vec4 {
let c = c.to_be_bytes();
color_rgba(c[0], c[1], c[2], c[3])
}

View file

@ -0,0 +1,12 @@
use kubi_shared::networking::state::ClientJoinState;
use shipyard::{UniqueViewMut, UniqueView};
use crate::state::{NextState, GameState};
pub fn switch_to_loading_if_connected(
mut next_state: UniqueViewMut<NextState>,
client_state: UniqueView<ClientJoinState>,
) {
if *client_state == ClientJoinState::Joined {
next_state.0 = Some(GameState::LoadingWorld);
}
}

View file

@ -1,12 +1,12 @@
use shipyard::{UniqueView, UniqueViewMut, Unique, AllStoragesView};
use glium::glutin::{event::VirtualKeyCode, event_loop::ControlFlow};
use crate::input::RawInputState;
use crate::input::RawKbmInputState;
#[derive(Unique)]
pub struct SetControlFlow(pub Option<ControlFlow>);
pub fn exit_on_esc(
raw_inputs: UniqueView<RawInputState>,
raw_inputs: UniqueView<RawKbmInputState>,
mut control_flow: UniqueViewMut<SetControlFlow>
) {
if raw_inputs.keyboard_state.contains(&VirtualKeyCode::Escape) {

View file

@ -3,7 +3,6 @@ use crate::rendering::Renderer;
use glium::glutin::window::CursorGrabMode;
#[derive(Unique)]
#[track(All)]
pub struct CursorLock(pub bool);
pub fn update_cursor_lock_state(

View file

@ -1,6 +1,7 @@
use glam::UVec2;
use shipyard::{World, Component, AllStoragesViewMut, SparseSet};
use shipyard::{World, Component, AllStoragesViewMut, SparseSet, NonSendSync, UniqueView};
use glium::glutin::event::{Event, DeviceEvent, DeviceId, WindowEvent};
use crate::rendering::Renderer;
pub mod player_actions;
@ -50,6 +51,19 @@ pub fn process_glutin_events(world: &mut World, event: &Event<'_, ()>) {
}
}
pub fn initial_resize_event(
mut storages: AllStoragesViewMut,
) {
let (w, h) = {
let renderer = storages.borrow::<NonSendSync<UniqueView<Renderer>>>().unwrap();
renderer.display.get_framebuffer_dimensions()
};
storages.add_entity((
EventComponent,
WindowResizedEvent(UVec2::new(w, h))
));
}
pub fn clear_events(
mut all_storages: AllStoragesViewMut,
) {

View file

@ -1,8 +1,7 @@
use shipyard::{Component, View, ViewMut, EntitiesViewMut, IntoIter};
use shipyard::{Component, View, ViewMut, EntitiesViewMut, IntoIter, track};
use glam::{IVec3, Quat, Vec3};
use kubi_shared::block::Block;
use crate::{
world::block::Block,
player::MainPlayer,
transform::Transform
};
@ -21,7 +20,7 @@ pub enum PlayerActionEvent {
}
pub fn generate_move_events(
transforms: View<Transform>,
transforms: View<Transform, { track::All }>,
player: View<MainPlayer>,
mut entities: EntitiesViewMut,
mut events: ViewMut<EventComponent>,

View file

@ -1,5 +1,5 @@
use glam::{Vec3, Mat4, Quat, EulerRot, Vec2};
use shipyard::{Component, View, ViewMut, IntoIter, UniqueView, Workload, IntoWorkload};
use shipyard::{Component, View, ViewMut, IntoIter, UniqueView, Workload, IntoWorkload, track};
use std::f32::consts::PI;
use crate::{transform::Transform, input::Inputs, settings::GameSettings, delta_time::DeltaTime};
@ -13,11 +13,11 @@ pub fn update_controllers() -> Workload {
).into_workload()
}
const MAX_PITCH: f32 = PI/2. - 0.025;
const MAX_PITCH: f32 = PI/2. - 0.05;
fn update_look(
controllers: View<FlyController>,
mut transforms: ViewMut<Transform>,
mut transforms: ViewMut<Transform, { track::All }>,
inputs: UniqueView<Inputs>,
settings: UniqueView<GameSettings>,
dt: UniqueView<DeltaTime>,
@ -37,12 +37,12 @@ fn update_look(
fn update_movement(
controllers: View<FlyController>,
mut transforms: ViewMut<Transform>,
mut transforms: ViewMut<Transform, { track::All }>,
inputs: UniqueView<Inputs>,
dt: UniqueView<DeltaTime>,
) {
if inputs.movement == Vec2::ZERO { return }
let movement = inputs.movement.normalize_or_zero() * 30. * dt.0.as_secs_f32();
let movement = inputs.movement * 30. * dt.0.as_secs_f32();
for (_, mut transform) in (&controllers, &mut transforms).iter() {
let (scale, rotation, mut translation) = transform.0.to_scale_rotation_translation();
translation += (rotation * Vec3::NEG_Z).normalize() * movement.y;

78
kubi/src/gui.rs Normal file
View file

@ -0,0 +1,78 @@
use shipyard::{Component, Unique, Workload, IntoWorkload, AllStoragesView, View, UniqueViewMut, IntoIter};
use glam::{Vec4, Mat4};
use crate::{color::color_hex, events::WindowResizedEvent};
pub mod text_widget;
pub mod progressbar;
use progressbar::render_progressbars;
//TODO compute gui scale on window resize
#[derive(Unique, Clone, Copy, Debug, Default)]
pub struct GuiView(pub Mat4);
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct GuiComponent;
#[derive(Component, Clone, Copy, Debug)]
pub struct PrimaryColor(pub Vec4);
impl Default for PrimaryColor {
fn default() -> Self {
Self(color_hex(0x156cddff))
}
}
#[derive(Component, Clone, Copy, Debug)]
pub struct SecondaryColor(pub Vec4);
impl Default for SecondaryColor {
fn default() -> Self {
Self(color_hex(0xc9d5e4ff))
}
}
fn update_gui_view(
mut view: UniqueViewMut<GuiView>,
resize: View<WindowResizedEvent>,
) {
let Some(&size) = resize.iter().next() else {
return
};
let [w, h] = size.0.to_array();
view.0 = Mat4::orthographic_rh_gl(0.0, w as f32, h as f32, 0.0, -1.0, 1.0);
}
pub fn init_gui(
storages: AllStoragesView
) {
storages.add_unique(GuiView::default());
}
pub fn update_gui() -> Workload {
(
update_gui_view
).into_workload()
}
pub fn render_gui() -> Workload {
(
render_progressbars
).into_workload()
}
// pub fn gui_testing(
// mut storages: AllStoragesViewMut,
// ) {
// storages.add_entity((
// GuiComponent,
// Transform2d(Mat3::from_scale_angle_translation(
// vec2(1920., 16.),
// 0.,
// vec2(0., 0.)
// )),
// ProgressbarComponent {
// progress: 0.33
// },
// PrimaryColor::default(),
// SecondaryColor::default(),
// ));
// }

View file

@ -0,0 +1,46 @@
use shipyard::{UniqueView, UniqueViewMut, NonSendSync, View, Component, IntoIter, IntoWithId, Get, track};
use glium::{Surface, uniform, DrawParameters};
use crate::{
prefabs::ProgressbarShaderPrefab,
rendering::{
RenderTarget,
primitives::rect::RectPrimitive
},
transform::Transform2d,
};
use super::{GuiComponent, PrimaryColor, SecondaryColor, GuiView};
#[derive(Component, Debug, Clone, Copy, Default)]
pub struct ProgressbarComponent {
pub progress: f32
}
pub fn render_progressbars(
mut target: NonSendSync<UniqueViewMut<RenderTarget>>,
rect: NonSendSync<UniqueView<RectPrimitive>>,
program: NonSendSync<UniqueView<ProgressbarShaderPrefab>>,
view: UniqueView<GuiView>,
components: View<GuiComponent>,
transforms: View<Transform2d, { track::All }>,
progressbars: View<ProgressbarComponent>,
primary: View<PrimaryColor>,
secondary: View<SecondaryColor>,
) {
for (eid, (_, transform, progress)) in (&components, &transforms, &progressbars).iter().with_id() {
let primary_color = primary.get(eid).copied().unwrap_or_default();
let secondary_color = secondary.get(eid).copied().unwrap_or_default();
target.0.draw(
&rect.0,
&rect.1,
&program.0,
&uniform! {
transform: transform.0.to_cols_array_2d(),
ui_view: view.0.to_cols_array_2d(),
progress: progress.progress,
color: primary_color.0.to_array(),
bg_color: secondary_color.0.to_array(),
},
&DrawParameters::default()
).unwrap();
}
}

View file

@ -0,0 +1 @@
//TODO text widget

21
kubi/src/init.rs Normal file
View file

@ -0,0 +1,21 @@
use shipyard::{AllStoragesView, UniqueViewMut};
use std::{env, net::SocketAddr};
use crate::{
networking::{GameType, ServerAddress},
state::{GameState, NextState}
};
pub fn initialize_from_args(
all_storages: AllStoragesView,
) {
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
let address = args[1].parse::<SocketAddr>().expect("invalid address");
all_storages.add_unique(GameType::Muliplayer);
all_storages.add_unique(ServerAddress(address));
all_storages.borrow::<UniqueViewMut<NextState>>().unwrap().0 = Some(GameState::Connecting);
} else {
all_storages.add_unique(GameType::Singleplayer);
all_storages.borrow::<UniqueViewMut<NextState>>().unwrap().0 = Some(GameState::LoadingWorld);
}
}

138
kubi/src/input.rs Normal file
View file

@ -0,0 +1,138 @@
use gilrs::{Gilrs, GamepadId, Button, Event, Axis};
use glam::{Vec2, DVec2, vec2};
use glium::glutin::event::{DeviceEvent, VirtualKeyCode, ElementState};
use hashbrown::HashSet;
use nohash_hasher::BuildNoHashHasher;
use shipyard::{AllStoragesView, Unique, View, IntoIter, UniqueViewMut, Workload, IntoWorkload, UniqueView, NonSendSync};
use crate::events::InputDeviceEvent;
#[derive(Unique, Clone, Copy, Default, Debug)]
pub struct Inputs {
pub movement: Vec2,
pub look: Vec2,
pub action_a: bool,
pub action_b: bool,
}
#[derive(Unique, Clone, Copy, Default, Debug)]
pub struct PrevInputs(pub Inputs);
#[derive(Unique, Clone, Default, Debug)]
pub struct RawKbmInputState {
pub keyboard_state: HashSet<VirtualKeyCode, BuildNoHashHasher<u32>>,
pub button_state: [bool; 32],
pub mouse_delta: DVec2
}
#[derive(Unique)]
pub struct GilrsWrapper(Gilrs);
#[derive(Unique, Default, Clone, Copy)]
pub struct ActiveGamepad(Option<GamepadId>);
//maybe we should manage gamepad state ourselves just like keyboard?
//at least for the sake of consitency
fn process_events(
device_events: View<InputDeviceEvent>,
mut input_state: UniqueViewMut<RawKbmInputState>,
) {
input_state.mouse_delta = DVec2::ZERO;
for event in device_events.iter() {
match event.event {
DeviceEvent::MouseMotion { delta } => {
input_state.mouse_delta = DVec2::from(delta);
},
DeviceEvent::Key(input) => {
if let Some(keycode) = input.virtual_keycode {
match input.state {
ElementState::Pressed => input_state.keyboard_state.insert(keycode),
ElementState::Released => input_state.keyboard_state.remove(&keycode),
};
}
},
DeviceEvent::Button { button, state } => {
if button < 32 {
input_state.button_state[button as usize] = matches!(state, ElementState::Pressed);
}
},
_ => ()
}
}
}
fn process_gilrs_events(
mut gilrs: NonSendSync<UniqueViewMut<GilrsWrapper>>,
mut active_gamepad: UniqueViewMut<ActiveGamepad>
) {
while let Some(Event { id, event: _, time: _ }) = gilrs.0.next_event() {
active_gamepad.0 = Some(id);
}
}
fn input_start(
mut inputs: UniqueViewMut<Inputs>,
mut prev_inputs: UniqueViewMut<PrevInputs>,
) {
prev_inputs.0 = *inputs;
*inputs = Inputs::default();
}
fn update_input_state (
raw_inputs: UniqueView<RawKbmInputState>,
mut inputs: UniqueViewMut<Inputs>,
) {
inputs.movement += Vec2::new(
raw_inputs.keyboard_state.contains(&VirtualKeyCode::D) as u32 as f32 -
raw_inputs.keyboard_state.contains(&VirtualKeyCode::A) as u32 as f32,
raw_inputs.keyboard_state.contains(&VirtualKeyCode::W) as u32 as f32 -
raw_inputs.keyboard_state.contains(&VirtualKeyCode::S) as u32 as f32
);
inputs.look += raw_inputs.mouse_delta.as_vec2();
inputs.action_a |= raw_inputs.button_state[1];
inputs.action_b |= raw_inputs.button_state[3];
}
fn update_input_state_gamepad (
gilrs: NonSendSync<UniqueView<GilrsWrapper>>,
active_gamepad: UniqueView<ActiveGamepad>,
mut inputs: UniqueViewMut<Inputs>,
) {
if let Some(gamepad) = active_gamepad.0.map(|id| gilrs.0.gamepad(id)) {
let left_stick = vec2(gamepad.value(Axis::LeftStickX), gamepad.value(Axis::LeftStickY));
let right_stick = vec2(gamepad.value(Axis::RightStickX), -gamepad.value(Axis::RightStickY));
inputs.movement += left_stick;
inputs.look += right_stick;
inputs.action_a |= gamepad.is_pressed(Button::South);
inputs.action_b |= gamepad.is_pressed(Button::East);
}
}
fn input_end(
mut inputs: UniqueViewMut<Inputs>,
) {
if inputs.movement.length() >= 1. {
inputs.movement = inputs.movement.normalize();
}
}
pub fn init_input (
storages: AllStoragesView
) {
storages.add_unique_non_send_sync(GilrsWrapper(Gilrs::new().expect("Failed to initialize Gilrs")));
storages.add_unique(ActiveGamepad::default());
storages.add_unique(Inputs::default());
storages.add_unique(PrevInputs::default());
storages.add_unique(RawKbmInputState::default());
}
pub fn process_inputs() -> Workload {
(
process_events,
process_gilrs_events,
input_start,
update_input_state,
update_input_state_gamepad,
input_end,
).into_workload()
}

104
kubi/src/loading_screen.rs Normal file
View file

@ -0,0 +1,104 @@
use shipyard::{UniqueView, UniqueViewMut, Workload, IntoWorkload, EntityId, Unique, AllStoragesViewMut, ViewMut, Get, SystemModificator, track};
use glium::glutin::event::VirtualKeyCode;
use glam::{Mat3, vec2};
use crate::{
world::ChunkStorage,
state::{GameState, NextState, is_changing_state},
transform::Transform2d,
gui::{
GuiComponent,
progressbar::ProgressbarComponent
},
rendering::{WindowSize, if_resized},
input::RawKbmInputState,
};
#[derive(Unique, Clone, Copy)]
struct ProgressbarId(EntityId);
fn spawn_loading_screen(
mut storages: AllStoragesViewMut,
) {
let size = *storages.borrow::<UniqueView<WindowSize>>().unwrap();
let entity = storages.add_entity((
GuiComponent,
Transform2d(Mat3::from_scale_angle_translation(
vec2(size.0.x as f32, 16.),
0.,
vec2(0., 0.)
)),
ProgressbarComponent {
progress: 0.33
},
));
storages.add_unique(ProgressbarId(entity));
}
fn resize_progress_bar(
size: UniqueView<WindowSize>,
bar: UniqueView<ProgressbarId>,
mut transforms: ViewMut<Transform2d, { track::All }>
) {
let mut trans = (&mut transforms).get(bar.0).unwrap();
trans.0.x_axis.x = size.0.x as f32;
}
fn update_progress_bar_progress (
world: UniqueView<ChunkStorage>,
mut bar: ViewMut<ProgressbarComponent>,
eid: UniqueView<ProgressbarId>,
) {
let mut bar = (&mut bar).get(eid.0).unwrap();
let loaded = world.chunks.iter().fold(0, |acc, (&_, chunk)| {
acc + chunk.desired_state.matches_current(chunk.current_state) as usize
});
let total = world.chunks.len();
let progress = loaded as f32 / total as f32;
bar.progress = progress;
}
fn switch_to_ingame_if_loaded(
world: UniqueView<ChunkStorage>,
mut state: UniqueViewMut<NextState>
) {
if world.chunks.is_empty() {
return
}
if world.chunks.iter().all(|(_, chunk)| {
chunk.desired_state.matches_current(chunk.current_state)
}) {
log::info!("Finished loading chunks");
state.0 = Some(GameState::InGame);
}
}
fn override_loading(
kbm_state: UniqueView<RawKbmInputState>,
mut state: UniqueViewMut<NextState>
) {
if kbm_state.keyboard_state.contains(&VirtualKeyCode::F) {
state.0 = Some(GameState::InGame);
}
}
fn despawn_loading_screen_if_switching_state(
mut storages: AllStoragesViewMut,
) {
let state = storages.borrow::<UniqueView<NextState>>().unwrap().0.unwrap();
if state != GameState::LoadingWorld {
let progress_bar = storages.borrow::<UniqueView<ProgressbarId>>().unwrap().0;
storages.delete_entity(progress_bar);
storages.remove_unique::<ProgressbarId>().unwrap();
}
}
pub fn update_loading_screen() -> Workload {
(
spawn_loading_screen.run_if_missing_unique::<ProgressbarId>(),
resize_progress_bar.run_if(if_resized),
update_progress_bar_progress,
override_loading,
switch_to_ingame_if_loaded,
despawn_loading_screen_if_switching_state.run_if(is_changing_state),
).into_workload()
}

View file

@ -1,10 +1,14 @@
// allowed because systems often need a lot of arguments
#![allow(clippy::too_many_arguments)]
#![cfg_attr(
all(windows, not(debug_assertions)),
windows_subsystem = "windows"
)]
#![allow(clippy::too_many_arguments)] // allowed because systems often need a lot of arguments
use shipyard::{
World, Workload, IntoWorkload,
UniqueView, UniqueViewMut,
NonSendSync
NonSendSync, WorkloadModificator,
SystemModificator
};
use glium::{
glutin::{
@ -15,13 +19,12 @@ use glium::{
use glam::vec3;
use std::time::Instant;
mod logging;
pub use kubi_shared::transform;
pub(crate) mod rendering;
pub(crate) mod world;
pub(crate) mod player;
pub(crate) mod prefabs;
pub(crate) mod transform;
pub(crate) mod settings;
pub(crate) mod camera;
pub(crate) mod events;
@ -31,19 +34,30 @@ pub(crate) mod block_placement;
pub(crate) mod delta_time;
pub(crate) mod cursor_lock;
pub(crate) mod control_flow;
pub(crate) mod state;
pub(crate) mod gui;
pub(crate) mod networking;
pub(crate) mod init;
pub(crate) mod color;
pub(crate) mod loading_screen;
pub(crate) mod connecting_screen;
use world::{
init_game_world,
loading::update_loaded_world_around_player,
raycast::update_raycasts
raycast::update_raycasts,
queue::apply_queued_blocks,
tasks::{inject_network_responses_into_manager_queue, ChunkTaskManager}, ChunkStorage
};
use player::spawn_player;
use player::{spawn_player, MainPlayer};
use prefabs::load_prefabs;
use settings::load_settings;
use camera::compute_cameras;
use events::{
clear_events, process_glutin_events,
player_actions::generate_move_events
clear_events,
process_glutin_events,
initial_resize_event,
player_actions::generate_move_events,
};
use input::{init_input, process_inputs};
use fly_controller::update_controllers;
@ -52,49 +66,86 @@ use rendering::{
RenderTarget,
BackgroundColor,
clear_background,
primitives::init_simple_box_buffers,
init_window_size,
update_window_size,
primitives::init_primitives,
selection_box::render_selection_box,
world::draw_world,
world::draw_current_chunk_border,
};
use block_placement::block_placement_system;
use block_placement::update_block_placement;
use delta_time::{DeltaTime, init_delta_time};
use cursor_lock::{insert_lock_state, update_cursor_lock_state, lock_cursor_now};
use control_flow::{exit_on_esc, insert_control_flow_unique, SetControlFlow};
use state::{is_ingame, is_ingame_or_loading, is_loading, init_state, update_state, is_connecting};
use networking::{update_networking, is_multiplayer, disconnect_on_exit};
use init::initialize_from_args;
use gui::{render_gui, init_gui, update_gui};
use loading_screen::update_loading_screen;
use connecting_screen::switch_to_loading_if_connected;
fn startup() -> Workload {
(
initial_resize_event,
init_window_size,
load_settings,
load_prefabs,
init_simple_box_buffers,
init_primitives,
insert_lock_state,
init_state,
initialize_from_args,
lock_cursor_now,
init_input,
init_game_world,
spawn_player,
init_gui,
insert_control_flow_unique,
init_delta_time,
).into_workload()
}
fn update() -> Workload {
(
process_inputs,
update_controllers,
generate_move_events,
update_loaded_world_around_player,
update_raycasts,
block_placement_system,
update_window_size,
update_cursor_lock_state,
process_inputs,
(
init_game_world.run_if_missing_unique::<ChunkTaskManager>(),
spawn_player.run_if_storage_empty::<MainPlayer>(),
).into_sequential_workload().run_if(is_ingame_or_loading).tag("game_init"),
(
update_networking,
inject_network_responses_into_manager_queue.run_if(is_ingame_or_loading).skip_if_missing_unique::<ChunkTaskManager>(),
).into_sequential_workload().run_if(is_multiplayer).tag("networking").after_all("game_init"),
(
switch_to_loading_if_connected
).into_workload().run_if(is_connecting).after_all("networking"),
(
update_loading_screen,
).into_workload().run_if(is_loading).after_all("game_init"),
(
update_loaded_world_around_player,
).into_workload().run_if(is_ingame_or_loading).after_all("game_init"),
(
update_controllers,
generate_move_events,
update_raycasts,
update_block_placement,
apply_queued_blocks,
).into_workload().run_if(is_ingame),
compute_cameras,
update_gui,
update_state,
exit_on_esc,
disconnect_on_exit.run_if(is_multiplayer),
).into_workload()
}
fn render() -> Workload {
(
clear_background,
draw_world,
draw_current_chunk_border,
render_selection_box,
(
draw_world,
draw_current_chunk_border,
render_selection_box,
).into_sequential_workload().run_if(is_ingame),
render_gui,
).into_sequential_workload()
}
fn after_frame_end() -> Workload {
@ -103,15 +154,28 @@ fn after_frame_end() -> Workload {
).into_workload()
}
fn main() {
logging::init();
#[cfg(all(windows, not(debug_assertions)))]
fn attach_console() {
use winapi::um::wincon::{AttachConsole, ATTACH_PARENT_PROCESS};
unsafe { AttachConsole(ATTACH_PARENT_PROCESS); }
}
//Create event loop
let event_loop = EventLoop::new();
fn main() {
//Attach console on release builds on windows
#[cfg(all(windows, not(debug_assertions)))] attach_console();
//Print version
println!("{:─^54}", format!("[ ▄▀ Kubi client v. {} ]", env!("CARGO_PKG_VERSION")));
//Init env_logger
kubi_logging::init();
//Create a shipyard world
let mut world = World::new();
//Create event loop
let event_loop = EventLoop::new();
//Add systems and uniques, Init and load things
world.add_unique_non_send_sync(Renderer::init(&event_loop));
world.add_unique(BackgroundColor(vec3(0.5, 0.5, 1.)));
@ -148,7 +212,7 @@ fn main() {
last_update = now;
}
//Run update workflow
//Run update workflows
world.run_workload(update).unwrap();
//Start rendering (maybe use custom views for this?)

169
kubi/src/networking.rs Normal file
View file

@ -0,0 +1,169 @@
use shipyard::{Unique, AllStoragesView, UniqueView, UniqueViewMut, Workload, IntoWorkload, EntitiesViewMut, Component, ViewMut, SystemModificator, View, IntoIter, WorkloadModificator};
use glium::glutin::event_loop::ControlFlow;
use std::net::SocketAddr;
use uflow::client::{Client, Config as ClientConfig, Event as ClientEvent};
use kubi_shared::networking::{
messages::{ClientToServerMessage, ServerToClientMessage, S_SERVER_HELLO},
state::ClientJoinState
};
use crate::{events::EventComponent, control_flow::SetControlFlow};
#[derive(Unique, Clone, Copy, PartialEq, Eq)]
pub enum GameType {
Singleplayer,
Muliplayer
}
#[derive(Unique, Clone, Copy, PartialEq, Eq)]
pub struct ServerAddress(pub SocketAddr);
#[derive(Unique)]
pub struct UdpClient(pub Client);
#[derive(Component)]
pub struct NetworkEvent(pub ClientEvent);
impl NetworkEvent {
///Checks if postcard-encoded message has a type
pub fn is_message_of_type<const T: u8>(&self) -> bool {
let ClientEvent::Receive(data) = &self.0 else { return false };
if data.len() == 0 { return false }
data[0] == T
}
}
#[derive(Component)]
pub struct NetworkMessageEvent(pub ServerToClientMessage);
fn connect_client(
storages: AllStoragesView
) {
log::info!("Creating client");
let address = storages.borrow::<UniqueView<ServerAddress>>().unwrap();
let client = Client::connect(address.0, ClientConfig::default()).expect("Client connection failed");
storages.add_unique(UdpClient(client));
storages.add_unique(ClientJoinState::Disconnected);
}
fn poll_client(
mut client: UniqueViewMut<UdpClient>,
mut entities: EntitiesViewMut,
mut events: ViewMut<EventComponent>,
mut network_events: ViewMut<NetworkEvent>,
) {
entities.bulk_add_entity((
&mut events,
&mut network_events,
), client.0.step().map(|event| {
(EventComponent, NetworkEvent(event))
}));
}
fn set_client_join_state_to_connected(
mut join_state: UniqueViewMut<ClientJoinState>
) {
log::info!("Setting ClientJoinState");
*join_state = ClientJoinState::Connected;
}
fn say_hello(
mut client: UniqueViewMut<UdpClient>,
) {
log::info!("Authenticating");
client.0.send(
postcard::to_allocvec(
&ClientToServerMessage::ClientHello {
username: "Sbeve".into(),
password: None
}
).unwrap().into_boxed_slice(),
0,
uflow::SendMode::Reliable
);
}
fn check_server_hello_response(
network_events: View<NetworkEvent>,
mut join_state: UniqueViewMut<ClientJoinState>
) {
for event in network_events.iter() {
let ClientEvent::Receive(data) = &event.0 else {
continue
};
if !event.is_message_of_type::<S_SERVER_HELLO>() {
continue
}
let Ok(parsed_message) = postcard::from_bytes(data) else {
log::error!("Malformed message");
continue
};
let ServerToClientMessage::ServerHello { init } = parsed_message else {
unreachable!()
};
//TODO handle init data
*join_state = ClientJoinState::Joined;
log::info!("Joined the server!");
return;
}
}
pub fn update_networking() -> Workload {
(
connect_client.run_if_missing_unique::<UdpClient>(),
poll_client,
(
set_client_join_state_to_connected,
say_hello,
).into_workload().run_if(if_just_connected),
(
check_server_hello_response,
).into_workload().run_if(is_join_state::<{ClientJoinState::Connected as u8}>)
).into_sequential_workload() //HACK Weird issues with shipyard removed
}
pub fn disconnect_on_exit(
control_flow: UniqueView<SetControlFlow>,
mut client: UniqueViewMut<UdpClient>,
) {
if let Some(ControlFlow::ExitWithCode(_)) = control_flow.0 {
if client.0.is_active() {
client.0.flush();
client.0.disconnect();
while client.0.is_active() { client.0.step().for_each(|_|()); }
log::info!("Client disconnected");
} else {
log::info!("Client inactive")
}
// if let Err(error) = client.0. {
// log::error!("failed to disconnect: {}", error);
// } else {
// log::info!("Client disconnected");
// }
}
}
// conditions
fn if_just_connected(
network_events: View<NetworkEvent>,
) -> bool {
network_events.iter().any(|event| matches!(&event.0, ClientEvent::Connect))
}
fn is_join_state<const STATE: u8>(
join_state: UniqueView<ClientJoinState>
) -> bool {
(*join_state as u8) == STATE
}
pub fn is_multiplayer(
game_type: UniqueView<GameType>
) -> bool {
*game_type == GameType::Muliplayer
}
pub fn is_singleplayer(
game_type: UniqueView<GameType>
) -> bool {
*game_type == GameType::Singleplayer
}

View file

@ -1,13 +1,12 @@
use shipyard::{Component, AllStoragesViewMut};
use shipyard::{Component, AllStoragesViewMut, View, IntoIter};
use crate::{
transform::Transform,
camera::Camera,
fly_controller::FlyController,
world::raycast::LookingAtBlock,
block_placement::PlayerHolding,
};
#[derive(Component)]
pub struct Player;
pub use kubi_shared::player::Player;
#[derive(Component)]
pub struct MainPlayer;
@ -16,12 +15,13 @@ pub fn spawn_player (
mut storages: AllStoragesViewMut
) {
log::info!("spawning player");
storages.add_entity((
let entity_id = storages.add_entity((
Player,
MainPlayer,
Transform::default(),
Camera::default(),
FlyController,
LookingAtBlock::default(),
PlayerHolding::default(),
));
}

View file

@ -1,6 +1,6 @@
use shipyard::{NonSendSync, UniqueView, Unique, AllStoragesView};
use glium::{texture::{SrgbTexture2dArray, MipmapsOption}, Program};
use strum::EnumIter;
use kubi_shared::block::{Block, BlockTexture};
use crate::rendering::Renderer;
mod texture;
@ -13,23 +13,6 @@ pub trait AssetPaths {
fn file_name(self) -> &'static str;
}
#[derive(Clone, Copy, Debug, EnumIter)]
#[repr(u8)]
pub enum BlockTexture {
Stone = 0,
Dirt = 1,
GrassTop = 2,
GrassSide = 3,
Sand = 4,
Bedrock = 5,
Wood = 6,
WoodTop = 7,
Leaf = 8,
Torch = 9,
TallGrass = 10,
Snow = 11,
GrassSideSnow = 12,
}
impl AssetPaths for BlockTexture {
fn file_name(self) -> &'static str {
match self {
@ -46,6 +29,9 @@ impl AssetPaths for BlockTexture {
Self::TallGrass => "tall_grass.png",
Self::Snow => "snow.png",
Self::GrassSideSnow => "grass_side_snow.png",
Self::Cobblestone => "cobblestone.png",
Self::Planks => "planks.png",
Self::WaterSolid => "solid_water.png",
}
}
}
@ -60,7 +46,10 @@ pub struct ChunkShaderPrefab(pub Program);
pub struct SelBoxShaderPrefab(pub Program);
#[derive(Unique)]
pub struct BasicColoredShaderPrefab(pub Program);
pub struct ColoredShaderPrefab(pub Program);
#[derive(Unique)]
pub struct ProgressbarShaderPrefab(pub Program);
pub fn load_prefabs(
storages: AllStoragesView,
@ -92,7 +81,7 @@ pub fn load_prefabs(
&renderer.display
)
));
storages.add_unique_non_send_sync(BasicColoredShaderPrefab(
storages.add_unique_non_send_sync(ColoredShaderPrefab(
include_shader_prefab!(
"colored",
"../shaders/colored.vert",
@ -100,4 +89,12 @@ pub fn load_prefabs(
&renderer.display
)
));
storages.add_unique_non_send_sync(ProgressbarShaderPrefab(
include_shader_prefab!(
"gui/progressbar",
"../shaders/gui/progressbar.vert",
"../shaders/gui/progressbar.frag",
&renderer.display
)
));
}

79
kubi/src/rendering.rs Normal file
View file

@ -0,0 +1,79 @@
use shipyard::{Unique, NonSendSync, UniqueView, UniqueViewMut, View, IntoIter, AllStoragesView};
use glium::{
Display, Surface,
Version, Api,
glutin::{
event_loop::EventLoop,
window::WindowBuilder,
ContextBuilder, GlProfile
},
};
use glam::{Vec3, UVec2};
use crate::events::WindowResizedEvent;
pub mod primitives;
pub mod world;
pub mod selection_box;
#[derive(Unique)]
pub struct RenderTarget(pub glium::Frame);
#[derive(Unique)]
pub struct BackgroundColor(pub Vec3);
#[derive(Unique, Clone, Copy)]
pub struct WindowSize(pub UVec2);
#[derive(Unique)]
pub struct Renderer {
pub display: Display
}
impl Renderer {
pub fn init(event_loop: &EventLoop<()>) -> Self {
log::info!("initializing display");
let wb = WindowBuilder::new()
.with_title("uwu")
.with_maximized(true);
let cb = ContextBuilder::new()
.with_depth_buffer(24)
.with_gl_profile(GlProfile::Core);
let display = Display::new(wb, cb, event_loop)
.expect("Failed to create a glium Display");
log::info!("Renderer: {}", display.get_opengl_renderer_string());
log::info!("OpenGL {}", display.get_opengl_version_string());
log::info!("Supports GLES {:?}", display.get_supported_glsl_version());
assert!(display.is_glsl_version_supported(&Version(Api::GlEs, 3, 0)), "GLES 3.0 is not supported");
Self { display }
}
}
pub fn clear_background(
mut target: NonSendSync<UniqueViewMut<RenderTarget>>,
color: UniqueView<BackgroundColor>,
) {
target.0.clear_color_srgb_and_depth((color.0.x, color.0.y, color.0.z, 1.), 1.);
}
//not sure if this belongs here
pub fn init_window_size(
storages: AllStoragesView,
) {
let size = storages.borrow::<View<WindowResizedEvent>>().unwrap().iter().next().unwrap().0;
storages.add_unique(WindowSize(size))
}
pub fn update_window_size(
mut win_size: UniqueViewMut<WindowSize>,
resize: View<WindowResizedEvent>,
) {
if let Some(resize) = resize.iter().next() {
win_size.0 = resize.0;
}
}
pub fn if_resized (
resize: View<WindowResizedEvent>,
) -> bool {
resize.len() > 0
}

View file

@ -0,0 +1,27 @@
use shipyard::{Workload, IntoWorkload};
use glium::implement_vertex;
pub mod cube;
pub mod rect;
use cube::init_cube_primitive;
use rect::init_rect_primitive;
#[derive(Clone, Copy, Default)]
pub struct PositionOnlyVertex {
pub position: [f32; 3],
}
implement_vertex!(PositionOnlyVertex, position);
#[derive(Clone, Copy, Default)]
pub struct PositionOnlyVertex2d {
pub position: [f32; 2],
}
implement_vertex!(PositionOnlyVertex2d, position);
pub fn init_primitives() -> Workload {
(
init_cube_primitive,
init_rect_primitive,
).into_workload()
}

View file

@ -0,0 +1,56 @@
use shipyard::{AllStoragesView, NonSendSync, UniqueView, Unique};
use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType};
use crate::rendering::Renderer;
use super::PositionOnlyVertex;
#[derive(Unique)]
pub struct CubePrimitive(pub VertexBuffer<PositionOnlyVertex>, pub IndexBuffer<u16>);
const CUBE_VERTICES: &[PositionOnlyVertex] = &[
// front
PositionOnlyVertex { position: [0.0, 0.0, 1.0] },
PositionOnlyVertex { position: [1.0, 0.0, 1.0] },
PositionOnlyVertex { position: [1.0, 1.0, 1.0] },
PositionOnlyVertex { position: [0.0, 1.0, 1.0] },
// back
PositionOnlyVertex { position: [0.0, 0.0, 0.0] },
PositionOnlyVertex { position: [1.0, 0.0, 0.0] },
PositionOnlyVertex { position: [1.0, 1.0, 0.0] },
PositionOnlyVertex { position: [0.0, 1.0, 0.0] },
];
const CUBE_INDICES: &[u16] = &[
// front
0, 1, 2,
2, 3, 0,
// right
1, 5, 6,
6, 2, 1,
// back
7, 6, 5,
5, 4, 7,
// left
4, 0, 3,
3, 7, 4,
// bottom
4, 5, 1,
1, 0, 4,
// top
3, 2, 6,
6, 7, 3
];
pub(super) fn init_cube_primitive(
storages: AllStoragesView,
display: NonSendSync<UniqueView<Renderer>>
) {
let vert = VertexBuffer::new(
&display.display,
CUBE_VERTICES
).unwrap();
let index = IndexBuffer::new(
&display.display,
PrimitiveType::TrianglesList,
CUBE_INDICES
).unwrap();
storages.add_unique_non_send_sync(CubePrimitive(vert, index));
}

View file

@ -0,0 +1,31 @@
use shipyard::{Unique, AllStoragesView, NonSendSync, UniqueView};
use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType};
use crate::rendering::Renderer;
use super::PositionOnlyVertex2d;
#[derive(Unique)]
pub struct RectPrimitive(pub VertexBuffer<PositionOnlyVertex2d>, pub IndexBuffer<u16>);
const RECT_VERTEX: &[PositionOnlyVertex2d] = &[
PositionOnlyVertex2d { position: [0., 0.] },
PositionOnlyVertex2d { position: [1., 0.] },
PositionOnlyVertex2d { position: [0., 1.] },
PositionOnlyVertex2d { position: [1., 1.] },
];
const RECT_INDEX: &[u16] = &[0, 1, 2, 1, 3, 2];
pub(super) fn init_rect_primitive(
storages: AllStoragesView,
display: NonSendSync<UniqueView<Renderer>>
) {
let vert = VertexBuffer::new(
&display.display,
RECT_VERTEX
).unwrap();
let index = IndexBuffer::new(
&display.display,
PrimitiveType::TrianglesList,
RECT_INDEX
).unwrap();
storages.add_unique_non_send_sync(RectPrimitive(vert, index));
}

View file

@ -12,7 +12,7 @@ use crate::{
};
use super::{
RenderTarget,
primitives::SimpleBoxBuffers,
primitives::cube::CubePrimitive,
};
pub fn render_selection_box(
@ -20,7 +20,7 @@ pub fn render_selection_box(
camera: View<Camera>,
mut target: NonSendSync<UniqueViewMut<RenderTarget>>,
program: NonSendSync<UniqueView<SelBoxShaderPrefab>>,
buffers: NonSendSync<UniqueView<SimpleBoxBuffers>>,
buffers: NonSendSync<UniqueView<CubePrimitive>>,
) {
let camera = camera.iter().next().unwrap();
let Some(lookat) = lookat.iter().next() else { return };

View file

@ -1,5 +1,5 @@
use glam::{Vec3, Mat4, Quat, ivec3};
use shipyard::{NonSendSync, UniqueView, UniqueViewMut, View, IntoIter};
use shipyard::{NonSendSync, UniqueView, UniqueViewMut, View, IntoIter, track};
use glium::{
implement_vertex, uniform,
Surface, DrawParameters,
@ -24,7 +24,7 @@ use crate::{
prefabs::{
ChunkShaderPrefab,
BlockTexturesPrefab,
BasicColoredShaderPrefab,
ColoredShaderPrefab,
},
world::{
ChunkStorage,
@ -32,9 +32,10 @@ use crate::{
chunk::CHUNK_SIZE,
}, settings::GameSettings,
};
use super::{RenderTarget, primitives::SimpleBoxBuffers};
use super::{RenderTarget, primitives::cube::CubePrimitive};
#[derive(Clone, Copy)]
#[repr(C)]
pub struct ChunkVertex {
pub position: [f32; 3],
pub normal: [f32; 3],
@ -43,7 +44,6 @@ pub struct ChunkVertex {
}
implement_vertex!(ChunkVertex, position, normal, uv, tex_index);
pub fn draw_world(
mut target: NonSendSync<UniqueViewMut<RenderTarget>>,
chunks: UniqueView<ChunkStorage>,
@ -109,13 +109,12 @@ pub fn draw_world(
}
}
//this doesn't use culling!
pub fn draw_current_chunk_border(
mut target: NonSendSync<UniqueViewMut<RenderTarget>>,
player: View<MainPlayer>,
transforms: View<Transform>,
buffers: NonSendSync<UniqueView<SimpleBoxBuffers>>,
program: NonSendSync<UniqueView<BasicColoredShaderPrefab>>,
transforms: View<Transform, { track::All }>,
buffers: NonSendSync<UniqueView<CubePrimitive>>,
program: NonSendSync<UniqueView<ColoredShaderPrefab>>,
camera: View<Camera>,
settings: UniqueView<GameSettings>,
) {

View file

@ -10,7 +10,7 @@ pub struct GameSettings {
impl Default for GameSettings {
fn default() -> Self {
Self {
render_distance: 5,
render_distance: 6,
mouse_sensitivity: 1.,
debug_draw_current_chunk_border: cfg!(debug_assertions),
}

58
kubi/src/state.rs Normal file
View file

@ -0,0 +1,58 @@
use shipyard::{Unique, UniqueView, UniqueViewMut, AllStoragesView};
use std::mem::take;
#[derive(Unique, PartialEq, Eq, Default, Clone, Copy)]
pub enum GameState {
#[default]
Initial,
Connecting,
LoadingWorld,
InGame
}
#[derive(Unique, PartialEq, Eq, Default, Clone, Copy)]
pub struct NextState(pub Option<GameState>);
pub fn init_state(
all_storages: AllStoragesView,
) {
all_storages.add_unique(GameState::default());
all_storages.add_unique(NextState::default());
}
pub fn update_state(
mut state: UniqueViewMut<GameState>,
mut next: UniqueViewMut<NextState>,
) {
*state = take(&mut next.0).unwrap_or(*state);
}
pub fn is_changing_state(
state: UniqueView<NextState>
) -> bool {
state.0.is_some()
}
pub fn is_connecting(
state: UniqueView<GameState>
) -> bool {
*state == GameState::Connecting
}
pub fn is_ingame(
state: UniqueView<GameState>
) -> bool {
*state == GameState::InGame
}
pub fn is_loading(
state: UniqueView<GameState>
) -> bool {
matches!(*state, GameState::LoadingWorld)
}
pub fn is_ingame_or_loading(
state: UniqueView<GameState>
) -> bool {
matches!(*state, GameState::InGame | GameState::LoadingWorld)
}

View file

@ -4,25 +4,21 @@ use glam::IVec3;
use hashbrown::HashMap;
use anyhow::{Result, Context};
pub use kubi_shared::{worldgen, block::Block};
pub mod chunk;
pub mod block;
pub mod tasks;
pub mod loading;
pub mod mesh;
pub mod neighbors;
pub mod worldgen;
pub mod raycast;
pub mod queue;
use chunk::{Chunk, ChunkMesh};
use chunk::{Chunk, ChunkMesh, CHUNK_SIZE};
use tasks::ChunkTaskManager;
use self::{chunk::CHUNK_SIZE, block::Block};
//TODO separate world struct for render data
// because this is not send-sync
use queue::BlockUpdateQueue;
#[derive(Default, Unique)]
#[track(Modification)]
pub struct ChunkStorage {
pub chunks: HashMap<IVec3, Chunk>
}
@ -109,4 +105,5 @@ pub fn init_game_world(
storages.add_unique_non_send_sync(ChunkMeshStorage::new());
storages.add_unique(ChunkStorage::new());
storages.add_unique(ChunkTaskManager::new());
storages.add_unique(BlockUpdateQueue::new());
}

View file

@ -1,11 +1,8 @@
use glam::IVec3;
use glium::{VertexBuffer, IndexBuffer};
use super::block::Block;
use crate::rendering::world::ChunkVertex;
pub const CHUNK_SIZE: usize = 32;
pub type BlockData = Box<[[[Block; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]>;
pub use kubi_shared::chunk::{CHUNK_SIZE, BlockData};
pub struct ChunkData {
pub blocks: BlockData,
@ -42,6 +39,13 @@ pub enum DesiredChunkState {
Rendered,
ToUnload,
}
impl DesiredChunkState {
pub fn matches_current(self, current: CurrentChunkState) -> bool {
(matches!(self, DesiredChunkState::Nothing) && matches!(current, CurrentChunkState::Nothing)) ||
(matches!(self, DesiredChunkState::Loaded) && matches!(current, CurrentChunkState::Loaded)) ||
(matches!(self, DesiredChunkState::Rendered) && matches!(current, CurrentChunkState::Rendered))
}
}
pub struct Chunk {
pub position: IVec3,
@ -49,7 +53,7 @@ pub struct Chunk {
pub mesh_index: Option<usize>,
pub current_state: CurrentChunkState,
pub desired_state: DesiredChunkState,
pub dirty: bool,
pub mesh_dirty: bool,
}
impl Chunk {
pub fn new(position: IVec3) -> Self {
@ -59,7 +63,7 @@ impl Chunk {
mesh_index: None,
current_state: Default::default(),
desired_state: Default::default(),
dirty: false,
mesh_dirty: false,
}
}
}

View file

@ -1,20 +1,26 @@
use glam::{IVec3, ivec3};
use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType};
use shipyard::{View, UniqueView, UniqueViewMut, IntoIter, Workload, IntoWorkload, NonSendSync};
use kubi_shared::networking::messages::ClientToServerMessage;
use shipyard::{View, UniqueView, UniqueViewMut, IntoIter, Workload, IntoWorkload, NonSendSync, track};
use kubi_shared::queue::QueuedBlock;
use uflow::SendMode;
use crate::{
player::MainPlayer,
transform::Transform,
settings::GameSettings,
rendering::Renderer
rendering::Renderer,
state::GameState,
networking::UdpClient,
};
use super::{
ChunkStorage, ChunkMeshStorage,
chunk::{Chunk, DesiredChunkState, CHUNK_SIZE, ChunkMesh, CurrentChunkState, ChunkData},
tasks::{ChunkTaskManager, ChunkTaskResponse, ChunkTask},
queue::BlockUpdateQueue
};
//todo limit task starts insted
const MAX_CHUNK_OPS: usize = 8;
const MAX_CHUNK_OPS_INGAME: usize = 6;
const MAX_CHUNK_OPS: usize = 32;
pub fn update_loaded_world_around_player() -> Workload {
(
@ -28,11 +34,11 @@ pub fn update_loaded_world_around_player() -> Workload {
pub fn update_chunks_if_player_moved(
v_settings: UniqueView<GameSettings>,
v_local_player: View<MainPlayer>,
v_transform: View<Transform>,
v_transform: View<Transform, { track::All }>,
mut vm_world: UniqueViewMut<ChunkStorage>,
) {
//Check if the player actually moved
//TODO fix this also triggers on rotation, only activate when the player crosses the chnk border
//TODO fix this also triggers on rotation, only activate when the player crosses the chunk border
let Some((_, transform)) = (&v_local_player, v_transform.inserted_or_modified()).iter().next() else {
return
};
@ -116,6 +122,7 @@ fn unload_downgrade_chunks(
fn start_required_tasks(
task_manager: UniqueView<ChunkTaskManager>,
mut udp_client: Option<UniqueViewMut<UdpClient>>,
mut world: UniqueViewMut<ChunkStorage>,
) {
if !world.is_modified() {
@ -128,17 +135,27 @@ fn start_required_tasks(
match chunk.desired_state {
DesiredChunkState::Loaded | DesiredChunkState::Rendered if chunk.current_state == CurrentChunkState::Nothing => {
//start load task
task_manager.spawn_task(ChunkTask::LoadChunk {
seed: 0xbeef_face_dead_cafe,
position
});
if let Some(client) = &mut udp_client {
client.0.send(
postcard::to_allocvec(&ClientToServerMessage::ChunkSubRequest {
chunk: position.to_array()
}).unwrap().into_boxed_slice(),
0,
SendMode::Reliable
);
} else {
task_manager.spawn_task(ChunkTask::LoadChunk {
seed: 0xbeef_face_dead_cafe,
position
});
}
//Update chunk state
let chunk = world.chunks.get_mut(&position).unwrap();
chunk.current_state = CurrentChunkState::Loading;
// ===========
//log::trace!("Started loading chunk {position}");
},
DesiredChunkState::Rendered if (chunk.current_state == CurrentChunkState::Loaded || chunk.dirty) => {
DesiredChunkState::Rendered if (chunk.current_state == CurrentChunkState::Loaded || chunk.mesh_dirty) => {
//get needed data
let Some(neighbors) = world.neighbors_all(position) else {
continue
@ -150,12 +167,12 @@ fn start_required_tasks(
task_manager.spawn_task(ChunkTask::GenerateMesh { data, position });
//Update chunk state
let chunk = world.chunks.get_mut(&position).unwrap();
if chunk.dirty {
if chunk.mesh_dirty {
chunk.current_state = CurrentChunkState::RecalculatingMesh;
} else {
chunk.current_state = CurrentChunkState::CalculatingMesh;
}
chunk.dirty = false;
chunk.mesh_dirty = false;
// ===========
//log::trace!("Started generating mesh for chunk {position}");
}
@ -168,63 +185,80 @@ fn process_completed_tasks(
task_manager: UniqueView<ChunkTaskManager>,
mut world: UniqueViewMut<ChunkStorage>,
mut meshes: NonSendSync<UniqueViewMut<ChunkMeshStorage>>,
renderer: NonSendSync<UniqueView<Renderer>>
renderer: NonSendSync<UniqueView<Renderer>>,
state: UniqueView<GameState>,
mut queue: UniqueViewMut<BlockUpdateQueue>,
) {
for _ in 0..MAX_CHUNK_OPS {
if let Some(res) = task_manager.receive() {
match res {
ChunkTaskResponse::LoadedChunk { position, chunk_data } => {
//check if chunk exists
let Some(chunk) = world.chunks.get_mut(&position) else {
log::warn!("blocks data discarded: chunk doesn't exist");
return
};
let mut ops: usize = 0;
while let Some(res) = task_manager.receive() {
match res {
ChunkTaskResponse::LoadedChunk { position, chunk_data, queued } => {
//check if chunk exists
let Some(chunk) = world.chunks.get_mut(&position) else {
log::warn!("blocks data discarded: chunk doesn't exist");
return
};
//check if chunk still wants it
if !matches!(chunk.desired_state, DesiredChunkState::Loaded | DesiredChunkState::Rendered) {
log::warn!("block data discarded: state undesirable: {:?}", chunk.desired_state);
return
}
//set the block data
chunk.block_data = Some(ChunkData {
blocks: chunk_data
});
//update chunk state
chunk.current_state = CurrentChunkState::Loaded;
},
ChunkTaskResponse::GeneratedMesh { position, vertices, indexes } => {
//check if chunk exists
let Some(chunk) = world.chunks.get_mut(&position) else {
log::warn!("mesh discarded: chunk doesn't exist");
return
};
//check if chunk still wants it
if chunk.desired_state != DesiredChunkState::Rendered {
log::warn!("mesh discarded: state undesirable: {:?}", chunk.desired_state);
return
}
//apply the mesh
let vertex_buffer = VertexBuffer::new(&renderer.display, &vertices).unwrap();
let index_buffer = IndexBuffer::new(&renderer.display, PrimitiveType::TrianglesList, &indexes).unwrap();
let mesh = ChunkMesh {
vertex_buffer,
index_buffer,
};
if let Some(index) = chunk.mesh_index {
meshes.update(index, mesh).expect("Mesh update failed");
} else {
let mesh_index = meshes.insert(mesh);
chunk.mesh_index = Some(mesh_index);
}
//update chunk state
chunk.current_state = CurrentChunkState::Rendered;
//check if chunk still wants it
if !matches!(chunk.desired_state, DesiredChunkState::Loaded | DesiredChunkState::Rendered) {
log::warn!("block data discarded: state undesirable: {:?}", chunk.desired_state);
return
}
//set the block data
chunk.block_data = Some(ChunkData {
blocks: chunk_data
});
//update chunk state
chunk.current_state = CurrentChunkState::Loaded;
//push queued blocks
//TODO use extend
for item in queued {
queue.push(item);
}
//increase ops counter
ops += 1;
},
ChunkTaskResponse::GeneratedMesh { position, vertices, indexes } => {
//check if chunk exists
let Some(chunk) = world.chunks.get_mut(&position) else {
log::warn!("mesh discarded: chunk doesn't exist");
return
};
//check if chunk still wants it
if chunk.desired_state != DesiredChunkState::Rendered {
log::warn!("mesh discarded: state undesirable: {:?}", chunk.desired_state);
return
}
//apply the mesh
let vertex_buffer = VertexBuffer::new(&renderer.display, &vertices).unwrap();
let index_buffer = IndexBuffer::new(&renderer.display, PrimitiveType::TrianglesList, &indexes).unwrap();
let mesh = ChunkMesh {
vertex_buffer,
index_buffer,
};
if let Some(index) = chunk.mesh_index {
meshes.update(index, mesh).expect("Mesh update failed");
} else {
let mesh_index = meshes.insert(mesh);
chunk.mesh_index = Some(mesh_index);
}
//update chunk state
chunk.current_state = CurrentChunkState::Rendered;
//increase ops counter
ops += 1;
}
}
if ops >= match *state {
GameState::InGame => MAX_CHUNK_OPS_INGAME,
_ => MAX_CHUNK_OPS,
} { break }
}
}

92
kubi/src/world/mesh.rs Normal file
View file

@ -0,0 +1,92 @@
use glam::{IVec3, ivec3};
use strum::IntoEnumIterator;
use kubi_shared::block::{Block, RenderType};
use crate::world::chunk::CHUNK_SIZE;
use crate::rendering::world::ChunkVertex;
pub mod data;
mod builder;
use data::MeshGenData;
use builder::{MeshBuilder, CubeFace, DiagonalFace};
pub fn generate_mesh(data: MeshGenData) -> (Vec<ChunkVertex>, Vec<u32>) {
let get_block = |pos: IVec3| -> Block {
if pos.x < 0 {
data.block_data_neg_x[(CHUNK_SIZE as i32 + pos.x) as usize][pos.y as usize][pos.z as usize]
} else if pos.x >= CHUNK_SIZE as i32 {
data.block_data_pos_x[pos.x as usize - CHUNK_SIZE][pos.y as usize][pos.z as usize]
} else if pos.y < 0 {
data.block_data_neg_y[pos.x as usize][(CHUNK_SIZE as i32 + pos.y) as usize][pos.z as usize]
} else if pos.y >= CHUNK_SIZE as i32 {
data.block_data_pos_y[pos.x as usize][pos.y as usize - CHUNK_SIZE][pos.z as usize]
} else if pos.z < 0 {
data.block_data_neg_z[pos.x as usize][pos.y as usize][(CHUNK_SIZE as i32 + pos.z) as usize]
} else if pos.z >= CHUNK_SIZE as i32 {
data.block_data_pos_z[pos.x as usize][pos.y as usize][pos.z as usize - CHUNK_SIZE]
} else {
data.block_data[pos.x as usize][pos.y as usize][pos.z as usize]
}
};
let mut builder = MeshBuilder::new();
for x in 0..CHUNK_SIZE as i32 {
for y in 0..CHUNK_SIZE as i32 {
for z in 0..CHUNK_SIZE as i32 {
let coord = ivec3(x, y, z);
let block = get_block(coord);
let descriptor = block.descriptor();
match descriptor.render {
RenderType::None => continue,
RenderType::SolidBlock(textures) | RenderType::BinaryTransparency(textures) => {
for face in CubeFace::iter() {
let facing_direction = face.normal();
let facing_coord = coord + facing_direction;
let facing_block = get_block(facing_coord);
let facing_descriptor = facing_block.descriptor();
let face_obstructed = match descriptor.render {
RenderType::SolidBlock(_) => matches!(facing_descriptor.render, RenderType::SolidBlock(_)),
RenderType::BinaryTransparency(_) => {
match facing_descriptor.render {
RenderType::SolidBlock(_) => true,
RenderType::BinaryTransparency(_) => block == facing_block,
_ => false,
}
},
_ => unreachable!(),
};
if !face_obstructed {
let face_texture = match face {
CubeFace::Top => textures.top,
CubeFace::Front => textures.front,
CubeFace::Left => textures.left,
CubeFace::Right => textures.right,
CubeFace::Back => textures.back,
CubeFace::Bottom => textures.bottom,
};
builder.add_face(face, coord, face_texture as u8);
}
}
},
RenderType::CrossShape(textures) => {
builder.add_diagonal_face(
coord,
DiagonalFace::LeftZ,
textures.0.front as u8,
textures.0.back as u8
);
builder.add_diagonal_face(
coord,
DiagonalFace::RigthZ,
textures.1.front as u8,
textures.1.back as u8
);
},
}
}
}
}
builder.finish()
}

View file

@ -0,0 +1,177 @@
use strum::EnumIter;
use glam::{Vec3, vec3, IVec3, ivec3};
use crate::rendering::world::ChunkVertex;
const INV_SQRT_2: f32 = 0.70710678118655; // 1 / 2.sqrt()
#[repr(usize)]
#[derive(Clone, Copy, Debug, EnumIter)]
pub enum CubeFace {
Top = 0,
Front = 4,
Left = 2,
Right = 3,
Back = 1,
Bottom = 5,
}
impl CubeFace {
pub const fn normal(self) -> IVec3 {
CUBE_FACE_NORMALS_IVEC3[self as usize]
}
}
const CUBE_FACE_VERTICES: [[Vec3; 4]; 6] = [
[vec3(0., 1., 0.), vec3(0., 1., 1.), vec3(1., 1., 0.), vec3(1., 1., 1.)],
[vec3(0., 0., 0.), vec3(0., 1., 0.), vec3(1., 0., 0.), vec3(1., 1., 0.)],
[vec3(0., 0., 1.), vec3(0., 1., 1.), vec3(0., 0., 0.), vec3(0., 1., 0.)],
[vec3(1., 0., 0.), vec3(1., 1., 0.), vec3(1., 0., 1.), vec3(1., 1., 1.)],
[vec3(1., 0., 1.), vec3(1., 1., 1.), vec3(0., 0., 1.), vec3(0., 1., 1.)],
[vec3(0., 0., 1.), vec3(0., 0., 0.), vec3(1., 0., 1.), vec3(1., 0., 0.)],
];
const CUBE_FACE_NORMALS_IVEC3: [IVec3; 6] = [
ivec3( 0, 1, 0),
ivec3( 0, 0, -1),
ivec3(-1, 0, 0),
ivec3( 1, 0, 0),
ivec3( 0, 0, 1),
ivec3( 0, -1, 0)
];
const CUBE_FACE_NORMALS: [Vec3; 6] = [
vec3(0., 1., 0.),
vec3(0., 0., -1.),
vec3(-1.,0., 0.),
vec3(1., 0., 0.),
vec3(0., 0., 1.),
vec3(0., -1.,0.)
];
const CUBE_FACE_INDICES: [u32; 6] = [0, 1, 2, 2, 1, 3];
#[repr(usize)]
pub enum DiagonalFace {
RigthZ = 0,
LeftZ = 1,
}
const CROSS_FACES: [[Vec3; 4]; 2] = [
[
vec3(0., 0., 0.),
vec3(0., 1., 0.),
vec3(1., 0., 1.),
vec3(1., 1., 1.),
],
[
vec3(0., 0., 1.),
vec3(0., 1., 1.),
vec3(1., 0., 0.),
vec3(1., 1., 0.),
]
];
const CROSS_FACE_NORMALS: [Vec3; 2] = [
vec3(-INV_SQRT_2, 0., INV_SQRT_2),
vec3(INV_SQRT_2, 0., INV_SQRT_2),
];
const CROSS_FACE_NORMALS_BACK: [Vec3; 2] = [
vec3(INV_SQRT_2, 0., -INV_SQRT_2),
vec3(-INV_SQRT_2, 0., -INV_SQRT_2),
];
const CROSS_FACE_INDICES: [u32; 12] = [
0, 1, 2, 2, 1, 3, //Front side
6, 5, 4, 7, 5, 6, //Back side
];
const UV_COORDS: [[f32; 2]; 4] = [
[0., 0.],
[0., 1.],
[1., 0.],
[1., 1.],
];
#[derive(Default)]
pub struct MeshBuilder {
vertex_buffer: Vec<ChunkVertex>,
index_buffer: Vec<u32>,
idx_counter: u32,
}
impl MeshBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn add_face(&mut self, face: CubeFace, coord: IVec3, texture: u8) {
let coord = coord.as_vec3();
let face_index = face as usize;
//Push vertices
let norm = CUBE_FACE_NORMALS[face_index];
let vert = CUBE_FACE_VERTICES[face_index];
self.vertex_buffer.reserve(4);
for i in 0..4 {
self.vertex_buffer.push(ChunkVertex {
position: (coord + vert[i]).to_array(),
normal: norm.to_array(),
uv: UV_COORDS[i],
tex_index: texture
});
}
//Push indices
self.index_buffer.extend_from_slice(&CUBE_FACE_INDICES.map(|x| x + self.idx_counter));
//Increment idx counter
self.idx_counter += 4;
}
pub fn add_diagonal_face(&mut self, coord: IVec3, face_type: DiagonalFace, front_texture: u8, back_texture: u8) {
//Push vertices
let face_type = face_type as usize;
let vertices = CROSS_FACES[face_type];
let normal_front = CROSS_FACE_NORMALS[face_type].to_array();
let normal_back = CROSS_FACE_NORMALS_BACK[face_type].to_array();
self.vertex_buffer.reserve(8);
for i in 0..4 { //push front vertices
self.vertex_buffer.push(ChunkVertex {
position: (coord.as_vec3() + vertices[i]).to_array(),
normal: normal_front,
uv: UV_COORDS[i],
tex_index: front_texture
})
}
for i in 0..4 { //push back vertices
self.vertex_buffer.push(ChunkVertex {
position: (coord.as_vec3() + vertices[i]).to_array(),
normal: normal_back,
uv: UV_COORDS[i],
tex_index: back_texture
})
}
//Push indices
self.index_buffer.extend_from_slice(&CROSS_FACE_INDICES.map(|x| x + self.idx_counter));
//Increment idx counter
self.idx_counter += 8;
}
pub fn add_model(&mut self, position: Vec3, vertices: &[ChunkVertex], indices: Option<&[u32]>) {
//push vertices
self.vertex_buffer.extend(vertices.iter().map(|vertex| {
let mut vertex = *vertex;
vertex.position[0] += position.x;
vertex.position[0] += position.y;
vertex.position[0] += position.z;
vertex
}));
//push indices
if let Some(indices) = indices {
self.index_buffer.extend(indices.iter().map(|x| x + self.idx_counter));
} else {
self.index_buffer.extend(0..(self.vertex_buffer.len() as u32));
}
//increment idx counter
self.idx_counter += vertices.len() as u32;
}
pub fn finish(self) -> (Vec<ChunkVertex>, Vec<u32>) {
(self.vertex_buffer, self.index_buffer)
}
}

57
kubi/src/world/queue.rs Normal file
View file

@ -0,0 +1,57 @@
use glam::{IVec3, ivec3};
use kubi_shared::{block::Block, chunk::CHUNK_SIZE, queue::QueuedBlock};
use shipyard::{UniqueViewMut, Unique};
use super::ChunkStorage;
#[derive(Unique, Default, Clone)]
pub struct BlockUpdateQueue {
queue: Vec<QueuedBlock>
}
impl BlockUpdateQueue {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, event: QueuedBlock) {
self.queue.push(event)
}
}
pub fn apply_queued_blocks(
mut queue: UniqueViewMut<BlockUpdateQueue>,
mut world: UniqueViewMut<ChunkStorage>
) {
//maybe i need to check for desired/current state here before marking as dirty?
queue.queue.retain(|&event| {
if let Some(block) = world.get_block_mut(event.position) {
if event.soft && *block != Block::Air {
return false
}
*block = event.block_type;
//mark chunk as dirty
let (chunk_pos, block_pos) = ChunkStorage::to_chunk_coords(event.position);
let chunk = world.chunks.get_mut(&chunk_pos).expect("This error should never happen, if it does then something is super fucked up and the whole project needs to be burnt down.");
chunk.mesh_dirty = true;
//If block pos is close to the border, some neighbors may be dirty!
const DIRECTIONS: [IVec3; 6] = [
ivec3(1, 0, 0),
ivec3(-1, 0, 0),
ivec3(0, 1, 0),
ivec3(0, -1, 0),
ivec3(0, 0, 1),
ivec3(0, 0, -1),
];
for direction in DIRECTIONS {
let outside_chunk = |x| !(0..CHUNK_SIZE as i32).contains(x);
let chunk_dirty = (block_pos + direction).to_array().iter().any(outside_chunk);
if chunk_dirty {
let dir_chunk_pos = chunk_pos + direction;
if let Some(dir_chunk) = world.chunks.get_mut(&dir_chunk_pos) {
dir_chunk.mesh_dirty = true;
}
}
}
return false
}
true
});
}

View file

@ -1,10 +1,10 @@
use glam::{Vec3, IVec3};
use shipyard::{View, Component, ViewMut, IntoIter, UniqueView};
use shipyard::{View, Component, ViewMut, IntoIter, UniqueView, track};
use kubi_shared::block::Block;
use crate::transform::Transform;
use super::ChunkStorage;
use super::{ChunkStorage, block::Block};
const RAYCAST_STEP: f32 = 0.25;
pub const RAYCAST_STEP: f32 = 0.25;
#[derive(Clone, Copy, Debug)]
pub struct RaycastReport {
@ -49,7 +49,7 @@ impl ChunkStorage {
pub struct LookingAtBlock(pub Option<RaycastReport>);
pub fn update_raycasts(
transform: View<Transform>,
transform: View<Transform, { track::All }>,
mut raycast: ViewMut<LookingAtBlock>,
world: UniqueView<ChunkStorage>,
) {
@ -57,9 +57,9 @@ pub fn update_raycasts(
if !(world.is_inserted_or_modified() || (transform.inserted_or_modified(), &raycast).iter().next().is_some()) {
return
}
for (transform, report) in (&transform, &mut raycast).iter() {
for (transform, mut report) in (&transform, &mut raycast).iter() {
let (_, rotation, position) = transform.0.to_scale_rotation_translation();
let direction = (rotation * Vec3::NEG_Z).normalize();
let direction = (rotation.normalize() * Vec3::NEG_Z).normalize();
*report = LookingAtBlock(world.raycast(position, direction, Some(30.)));
}
}

106
kubi/src/world/tasks.rs Normal file
View file

@ -0,0 +1,106 @@
use flume::{Sender, Receiver};
use glam::IVec3;
use kubi_shared::{
networking::messages::{S_CHUNK_RESPONSE, ServerToClientMessage},
queue::QueuedBlock
};
use shipyard::{Unique, UniqueView, View, IntoIter};
use rayon::{ThreadPool, ThreadPoolBuilder};
use uflow::client::Event as ClientEvent;
use super::{
chunk::BlockData,
mesh::{generate_mesh, data::MeshGenData},
worldgen::generate_world,
};
use crate::{
rendering::world::ChunkVertex,
networking::NetworkEvent,
};
pub enum ChunkTask {
LoadChunk {
seed: u64,
position: IVec3
},
GenerateMesh {
position: IVec3,
data: MeshGenData
}
}
pub enum ChunkTaskResponse {
LoadedChunk {
position: IVec3,
chunk_data: BlockData,
queued: Vec<QueuedBlock>
},
GeneratedMesh {
position: IVec3,
vertices: Vec<ChunkVertex>,
indexes: Vec<u32>
},
}
#[derive(Unique)]
pub struct ChunkTaskManager {
channel: (Sender<ChunkTaskResponse>, Receiver<ChunkTaskResponse>),
pool: ThreadPool,
}
impl ChunkTaskManager {
pub fn new() -> Self {
Self {
channel: flume::unbounded::<ChunkTaskResponse>(), //maybe put a bound or even bound(0)?
pool: ThreadPoolBuilder::new().num_threads(4).build().unwrap()
}
}
pub fn add_sussy_response(&self, response: ChunkTaskResponse) {
// this WILL get stuck if the channel is bounded
// don't make the channel bounded ever
self.channel.0.send(response).unwrap()
}
pub fn spawn_task(&self, task: ChunkTask) {
let sender = self.channel.0.clone();
self.pool.spawn(move || {
let _ = sender.send(match task {
ChunkTask::GenerateMesh { position, data } => {
let (vertices, indexes) = generate_mesh(data);
ChunkTaskResponse::GeneratedMesh { position, vertices, indexes }
},
ChunkTask::LoadChunk { position, seed } => {
let (chunk_data, queued) = generate_world(position, seed);
ChunkTaskResponse::LoadedChunk { position, chunk_data, queued }
}
});
});
}
pub fn receive(&self) -> Option<ChunkTaskResponse> {
self.channel.1.try_recv().ok()
}
}
//TODO get rid of this, this is awfulll
pub fn inject_network_responses_into_manager_queue(
manager: UniqueView<ChunkTaskManager>,
events: View<NetworkEvent>
) {
for event in events.iter() {
if event.is_message_of_type::<S_CHUNK_RESPONSE>() {
let NetworkEvent(ClientEvent::Receive(data)) = &event else { unreachable!() };
let ServerToClientMessage::ChunkResponse {
chunk, data, queued
} = postcard::from_bytes(data).expect("Chunk decode failed") else { unreachable!() };
manager.add_sussy_response(ChunkTaskResponse::LoadedChunk {
position: IVec3::from_array(chunk),
chunk_data: data,
queued
});
}
// if let ClientEvent::MessageReceived(ServerToClientMessage::ChunkResponse { &chunk, data, queued }) = &event.0 {
// let position = IVec3::from_array(chunk);
// manager.add_sussy_response(ChunkTaskResponse::LoadedChunk {
// position,
// chunk_data: data.clone(),
// queued
// });
// }
}
}

View file

@ -1,9 +0,0 @@
#version 150 core
out vec4 color;
uniform vec4 u_color;
void main() {
color = u_color;
color -= vec4(0, 0, 0, 0.1 * sin(gl_FragCoord.x) * cos(gl_FragCoord.y));
}

View file

@ -1,48 +0,0 @@
use shipyard::{UniqueViewMut, UniqueView, View, IntoIter, ViewMut, EntitiesViewMut};
use crate::{
player::MainPlayer,
world::{raycast::LookingAtBlock, ChunkStorage, block::Block},
input::{Inputs, PrevInputs},
events::{EventComponent, player_actions::PlayerActionEvent},
};
pub fn block_placement_system(
main_player: View<MainPlayer>,
raycast: View<LookingAtBlock>,
input: UniqueView<Inputs>,
prev_input: UniqueView<PrevInputs>,
mut world: UniqueViewMut<ChunkStorage>,
mut entities: EntitiesViewMut,
mut events: ViewMut<EventComponent>,
mut player_events: ViewMut<PlayerActionEvent>,
) {
let action_place = input.action_b && !prev_input.0.action_b;
let action_break = input.action_a && !prev_input.0.action_a;
if action_place ^ action_break {
//get raycast info
let Some(ray) = (&main_player, &raycast).iter().next().unwrap().1/**/.0 else { return };
//update block
let (place_position, place_block) = if action_place {
let position = (ray.position - ray.direction * 0.5).floor().as_ivec3();
let Some(block) = world.get_block_mut(position) else { return };
*block = Block::Dirt;
(position, *block)
} else {
let Some(block) = world.get_block_mut(ray.block_position) else { return };
*block = Block::Air;
(ray.block_position, *block)
};
//mark chunk as dirty
let (chunk_pos, _) = ChunkStorage::to_chunk_coords(place_position);
let chunk = world.chunks.get_mut(&chunk_pos).unwrap();
chunk.dirty = true;
//send event
entities.add_entity(
(&mut events, &mut player_events),
(EventComponent, PlayerActionEvent::UpdatedBlock {
position: place_position,
block: place_block,
})
);
}
}

View file

@ -1,42 +0,0 @@
use glam::{Vec3, Mat4};
use shipyard::{ViewMut, View, IntoIter, Workload, IntoWorkload};
use crate::{transform::Transform, events::WindowResizedEvent};
use super::Camera;
//maybe parallelize these two?
fn update_view_matrix(
mut vm_camera: ViewMut<Camera>,
v_transform: View<Transform>
) {
for (camera, transform) in (&mut vm_camera, v_transform.inserted_or_modified()).iter() {
let (_, rotation, translation) = transform.0.to_scale_rotation_translation();
let direction = rotation * Vec3::NEG_Z;
camera.view_matrix = Mat4::look_to_rh(translation, direction, camera.up);
}
}
fn update_perspective_matrix(
mut vm_camera: ViewMut<Camera>,
resize: View<WindowResizedEvent>,
) {
//TODO update on launch
let Some(&size) = resize.iter().next() else {
return
};
for camera in (&mut vm_camera).iter() {
camera.perspective_matrix = Mat4::perspective_rh_gl(
camera.fov,
size.0.x as f32 / size.0.y as f32,
camera.z_near,
camera.z_far,
)
}
}
pub fn update_matrices() -> Workload {
(
update_view_matrix,
update_perspective_matrix,
).into_workload()
}

View file

@ -1,84 +0,0 @@
use glam::{Vec2, DVec2};
use glium::glutin::event::{DeviceEvent, VirtualKeyCode, ElementState};
use hashbrown::HashSet;
use nohash_hasher::BuildNoHashHasher;
use shipyard::{AllStoragesView, Unique, View, IntoIter, UniqueViewMut, Workload, IntoWorkload, UniqueView};
use crate::events::InputDeviceEvent;
#[derive(Unique, Clone, Copy, Default, Debug)]
pub struct Inputs {
pub movement: Vec2,
pub look: Vec2,
pub action_a: bool,
pub action_b: bool,
}
#[derive(Unique, Clone, Copy, Default, Debug)]
pub struct PrevInputs(pub Inputs);
#[derive(Unique, Clone, Default, Debug)]
pub struct RawInputState {
pub keyboard_state: HashSet<VirtualKeyCode, BuildNoHashHasher<u32>>,
pub button_state: [bool; 32],
pub mouse_delta: DVec2
}
pub fn process_events(
device_events: View<InputDeviceEvent>,
mut input_state: UniqueViewMut<RawInputState>,
) {
input_state.mouse_delta = DVec2::ZERO;
for event in device_events.iter() {
match event.event {
DeviceEvent::MouseMotion { delta } => {
input_state.mouse_delta = DVec2::from(delta);
},
DeviceEvent::Key(input) => {
if let Some(keycode) = input.virtual_keycode {
match input.state {
ElementState::Pressed => input_state.keyboard_state.insert(keycode),
ElementState::Released => input_state.keyboard_state.remove(&keycode),
};
}
},
DeviceEvent::Button { button, state } => {
if button < 32 {
input_state.button_state[button as usize] = matches!(state, ElementState::Pressed);
}
},
_ => ()
}
}
}
pub fn update_input_states (
raw_inputs: UniqueView<RawInputState>,
mut inputs: UniqueViewMut<Inputs>,
mut prev_inputs: UniqueViewMut<PrevInputs>,
) {
prev_inputs.0 = *inputs;
inputs.movement = Vec2::new(
raw_inputs.keyboard_state.contains(&VirtualKeyCode::D) as u32 as f32 -
raw_inputs.keyboard_state.contains(&VirtualKeyCode::A) as u32 as f32,
raw_inputs.keyboard_state.contains(&VirtualKeyCode::W) as u32 as f32 -
raw_inputs.keyboard_state.contains(&VirtualKeyCode::S) as u32 as f32
).normalize_or_zero();
inputs.look = raw_inputs.mouse_delta.as_vec2();
inputs.action_a = raw_inputs.button_state[1];
inputs.action_b = raw_inputs.button_state[3];
}
pub fn init_input (
storages: AllStoragesView
) {
storages.add_unique(Inputs::default());
storages.add_unique(PrevInputs::default());
storages.add_unique(RawInputState::default());
}
pub fn process_inputs() -> Workload {
(
process_events,
update_input_states
).into_workload()
}

View file

@ -1,46 +0,0 @@
use shipyard::{Unique, NonSendSync, UniqueView, UniqueViewMut};
use glium::{
Display, Surface,
glutin::{
event_loop::EventLoop,
window::WindowBuilder,
ContextBuilder, GlProfile
},
};
use glam::Vec3;
pub mod primitives;
pub mod world;
pub mod selection_box;
#[derive(Unique)]
pub struct RenderTarget(pub glium::Frame);
#[derive(Unique)]
pub struct BackgroundColor(pub Vec3);
#[derive(Unique)]
pub struct Renderer {
pub display: Display
}
impl Renderer {
pub fn init(event_loop: &EventLoop<()>) -> Self {
log::info!("initializing display");
let wb = WindowBuilder::new()
.with_title("uwu")
.with_maximized(true);
let cb = ContextBuilder::new()
.with_depth_buffer(24)
.with_gl_profile(GlProfile::Core);
let display = Display::new(wb, cb, event_loop)
.expect("Failed to create a glium Display");
Self { display }
}
}
pub fn clear_background(
mut target: NonSendSync<UniqueViewMut<RenderTarget>>,
color: UniqueView<BackgroundColor>,
) {
target.0.clear_color_srgb_and_depth((color.0.x, color.0.y, color.0.z, 1.), 1.);
}

View file

@ -1,80 +0,0 @@
use glium::{implement_vertex, VertexBuffer, IndexBuffer, index::PrimitiveType};
use shipyard::{NonSendSync, UniqueView, AllStoragesView, Unique};
use super::Renderer;
pub const CUBE_VERTICES: &[f32] = &[
// front
0.0, 0.0, 1.0,
1.0, 0.0, 1.0,
1.0, 1.0, 1.0,
0.0, 1.0, 1.0,
// back
0.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 1.0, 0.0,
0.0, 1.0, 0.0
];
pub const CUBE_INDICES: &[u16] = &[
// front
0, 1, 2,
2, 3, 0,
// right
1, 5, 6,
6, 2, 1,
// back
7, 6, 5,
5, 4, 7,
// left
4, 0, 3,
3, 7, 4,
// bottom
4, 5, 1,
1, 0, 4,
// top
3, 2, 6,
6, 7, 3
];
#[derive(Clone, Copy, Default)]
pub struct PositionOnlyVertex {
pub position: [f32; 3],
}
implement_vertex!(PositionOnlyVertex, position);
const fn box_vertices() -> [PositionOnlyVertex; CUBE_VERTICES.len() / 3] {
let mut arr = [PositionOnlyVertex { position: [0., 0., 0.] }; CUBE_VERTICES.len() / 3];
let mut ptr = 0;
loop {
arr[ptr] = PositionOnlyVertex {
position: [
CUBE_VERTICES[ptr * 3],
CUBE_VERTICES[(ptr * 3) + 1],
CUBE_VERTICES[(ptr * 3) + 2]
]
};
ptr += 1;
if ptr >= CUBE_VERTICES.len() / 3 {
return arr
}
}
}
const BOX_VERTICES: &[PositionOnlyVertex] = &box_vertices();
#[derive(Unique)]
pub struct SimpleBoxBuffers(pub VertexBuffer<PositionOnlyVertex>, pub IndexBuffer<u16>);
pub fn init_simple_box_buffers(
storages: AllStoragesView,
display: NonSendSync<UniqueView<Renderer>>
) {
let vert = VertexBuffer::new(
&display.display,
BOX_VERTICES
).unwrap();
let index = IndexBuffer::new(
&display.display,
PrimitiveType::TrianglesList,
CUBE_INDICES
).unwrap();
storages.add_unique_non_send_sync(SimpleBoxBuffers(vert, index));
}

View file

@ -1,6 +0,0 @@
use shipyard::Component;
use glam::Mat4;
#[derive(Component, Clone, Copy, Debug, Default)]
#[track(All)]
pub struct Transform(pub Mat4);

View file

@ -1,105 +0,0 @@
use strum::EnumIter;
use crate::prefabs::BlockTexture;
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)]
#[repr(u8)]
pub enum Block {
Air,
Stone,
Dirt,
Grass,
Sand,
}
impl Block {
pub const fn descriptor(self) -> BlockDescriptor {
match self {
Self::Air => BlockDescriptor {
name: "air",
render: RenderType::None,
collision: CollisionType::None,
raycast_collision: false,
},
Self::Stone => BlockDescriptor {
name: "stone",
render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Stone)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::Dirt => BlockDescriptor {
name: "dirt",
render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Dirt)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::Grass => BlockDescriptor {
name: "grass",
render: RenderType::SolidBlock(CubeTexture::top_sides_bottom(
BlockTexture::GrassTop,
BlockTexture::GrassSide,
BlockTexture::Dirt
)),
collision: CollisionType::Solid,
raycast_collision: true,
},
Self::Sand => BlockDescriptor {
name: "sand",
render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Sand)),
collision: CollisionType::Solid,
raycast_collision: true,
},
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct BlockDescriptor {
pub name: &'static str,
pub render: RenderType,
pub collision: CollisionType,
pub raycast_collision: bool,
}
// impl BlockDescriptor {
// pub fn of(block: Block) -> Self {
// block.descriptor()
// }
// }
#[derive(Clone, Copy, Debug)]
pub struct CubeTexture {
pub top: BlockTexture,
pub bottom: BlockTexture,
pub left: BlockTexture,
pub right: BlockTexture,
pub front: BlockTexture,
pub back: BlockTexture,
}
impl CubeTexture {
pub const fn top_sides_bottom(top: BlockTexture, sides: BlockTexture, bottom: BlockTexture) -> Self {
Self {
top,
bottom,
left: sides,
right: sides,
front: sides,
back: sides,
}
}
pub const fn horizontal_vertical(horizontal: BlockTexture, vertical: BlockTexture) -> Self {
Self::top_sides_bottom(vertical, horizontal, vertical)
}
pub const fn all(texture: BlockTexture) -> Self {
Self::horizontal_vertical(texture, texture)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CollisionType {
None,
Solid,
}
#[derive(Clone, Copy, Debug)]
pub enum RenderType {
None,
SolidBlock(CubeTexture)
}

View file

@ -1,138 +0,0 @@
use strum::{EnumIter, IntoEnumIterator};
use glam::{Vec3A, vec3a, IVec3, ivec3};
use std::mem::discriminant;
use super::{chunk::CHUNK_SIZE, block::{Block, RenderType}};
use crate::rendering::world::ChunkVertex;
pub mod data;
use data::MeshGenData;
#[repr(usize)]
#[derive(Clone, Copy, Debug, EnumIter)]
pub enum CubeFace {
Top = 0,
Front = 1,
Left = 2,
Right = 3,
Back = 4,
Bottom = 5,
}
const CUBE_FACE_VERTICES: [[Vec3A; 4]; 6] = [
[vec3a(0., 1., 0.), vec3a(0., 1., 1.), vec3a(1., 1., 0.), vec3a(1., 1., 1.)],
[vec3a(0., 0., 0.), vec3a(0., 1., 0.), vec3a(1., 0., 0.), vec3a(1., 1., 0.)],
[vec3a(0., 0., 1.), vec3a(0., 1., 1.), vec3a(0., 0., 0.), vec3a(0., 1., 0.)],
[vec3a(1., 0., 0.), vec3a(1., 1., 0.), vec3a(1., 0., 1.), vec3a(1., 1., 1.)],
[vec3a(1., 0., 1.), vec3a(1., 1., 1.), vec3a(0., 0., 1.), vec3a(0., 1., 1.)],
[vec3a(0., 0., 1.), vec3a(0., 0., 0.), vec3a(1., 0., 1.), vec3a(1., 0., 0.)],
];
const CUBE_FACE_NORMALS: [Vec3A; 6] = [
vec3a(0., 1., 0.),
vec3a(0., 0., -1.),
vec3a(-1.,0., 0.),
vec3a(1., 0., 0.),
vec3a(0., 0., 1.),
vec3a(0., -1.,0.)
];
const CUBE_FACE_INDICES: [u32; 6] = [0, 1, 2, 2, 1, 3];
const UV_COORDS: [[f32; 2]; 4] = [
[0., 0.],
[0., 1.],
[1., 0.],
[1., 1.],
];
#[derive(Default)]
struct MeshBuilder {
vertex_buffer: Vec<ChunkVertex>,
index_buffer: Vec<u32>,
idx_counter: u32,
}
impl MeshBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn add_face(&mut self, face: CubeFace, coord: IVec3, texture: u8) {
let coord = coord.as_vec3a();
let face_index = face as usize;
//Push vertexes
let norm = CUBE_FACE_NORMALS[face_index];
let vert = CUBE_FACE_VERTICES[face_index];
self.vertex_buffer.reserve(4);
for i in 0..4 {
self.vertex_buffer.push(ChunkVertex {
position: (coord + vert[i]).to_array(),
normal: norm.to_array(),
uv: UV_COORDS[i],
tex_index: texture
});
}
//Push indices
self.index_buffer.extend_from_slice(&CUBE_FACE_INDICES.map(|x| x + self.idx_counter));
self.idx_counter += 4;
}
pub fn finish(self) -> (Vec<ChunkVertex>, Vec<u32>) {
(self.vertex_buffer, self.index_buffer)
}
}
pub fn generate_mesh(data: MeshGenData) -> (Vec<ChunkVertex>, Vec<u32>) {
let get_block = |pos: IVec3| -> Block {
if pos.x < 0 {
data.block_data_neg_x[(CHUNK_SIZE as i32 + pos.x) as usize][pos.y as usize][pos.z as usize]
} else if pos.x >= CHUNK_SIZE as i32 {
data.block_data_pos_x[pos.x as usize - CHUNK_SIZE][pos.y as usize][pos.z as usize]
} else if pos.y < 0 {
data.block_data_neg_y[pos.x as usize][(CHUNK_SIZE as i32 + pos.y) as usize][pos.z as usize]
} else if pos.y >= CHUNK_SIZE as i32 {
data.block_data_pos_y[pos.x as usize][pos.y as usize - CHUNK_SIZE][pos.z as usize]
} else if pos.z < 0 {
data.block_data_neg_z[pos.x as usize][pos.y as usize][(CHUNK_SIZE as i32 + pos.z) as usize]
} else if pos.z >= CHUNK_SIZE as i32 {
data.block_data_pos_z[pos.x as usize][pos.y as usize][pos.z as usize - CHUNK_SIZE]
} else {
data.block_data[pos.x as usize][pos.y as usize][pos.z as usize]
}
};
let mut builder = MeshBuilder::new();
for x in 0..CHUNK_SIZE {
for y in 0..CHUNK_SIZE {
for z in 0..CHUNK_SIZE {
let coord = ivec3(x as i32, y as i32, z as i32);
let block = get_block(coord);
let descriptor = block.descriptor();
if matches!(descriptor.render, RenderType::None) {
continue
}
for face in CubeFace::iter() {
let facing = CUBE_FACE_NORMALS[face as usize].as_ivec3();
let facing_coord = coord + facing;
let show = discriminant(&get_block(facing_coord).descriptor().render) != discriminant(&descriptor.render);
if show {
match descriptor.render {
RenderType::SolidBlock(textures) => {
let face_texture = match face {
CubeFace::Top => textures.top,
CubeFace::Front => textures.front,
CubeFace::Left => textures.left,
CubeFace::Right => textures.right,
CubeFace::Back => textures.back,
CubeFace::Bottom => textures.bottom,
};
builder.add_face(face, coord, face_texture as u8);
},
_ => unimplemented!()
}
}
}
}
}
}
builder.finish()
}

View file

@ -1,64 +0,0 @@
use flume::{Sender, Receiver};
use glam::IVec3;
use shipyard::Unique;
use rayon::{ThreadPool, ThreadPoolBuilder};
use super::{
chunk::BlockData,
mesh::{generate_mesh, data::MeshGenData},
worldgen::generate_world,
};
use crate::rendering::world::ChunkVertex;
pub enum ChunkTask {
LoadChunk {
seed: u64,
position: IVec3
},
GenerateMesh {
position: IVec3,
data: MeshGenData
}
}
pub enum ChunkTaskResponse {
LoadedChunk {
position: IVec3,
chunk_data: BlockData,
},
GeneratedMesh {
position: IVec3,
vertices: Vec<ChunkVertex>,
indexes: Vec<u32>
},
}
#[derive(Unique)]
pub struct ChunkTaskManager {
channel: (Sender<ChunkTaskResponse>, Receiver<ChunkTaskResponse>),
pool: ThreadPool,
}
impl ChunkTaskManager {
pub fn new() -> Self {
Self {
channel: flume::unbounded::<ChunkTaskResponse>(), //maybe put a bound or even bound(0)?
pool: ThreadPoolBuilder::new().num_threads(4).build().unwrap()
}
}
pub fn spawn_task(&self, task: ChunkTask) {
let sender = self.channel.0.clone();
self.pool.spawn(move || {
let _ = sender.send(match task {
ChunkTask::GenerateMesh { position, data } => {
let (vertices, indexes) = generate_mesh(data);
ChunkTaskResponse::GeneratedMesh { position, vertices, indexes }
},
ChunkTask::LoadChunk { position, seed } => {
let chunk_data = generate_world(position, seed);
ChunkTaskResponse::LoadedChunk { position, chunk_data }
}
});
});
}
pub fn receive(&self) -> Option<ChunkTaskResponse> {
self.channel.1.try_recv().ok()
}
}

View file

@ -1,49 +0,0 @@
use glam::{IVec3, ivec3};
use bracket_noise::prelude::*;
use super::{
chunk::{BlockData, CHUNK_SIZE},
block::Block
};
pub fn generate_world(chunk_position: IVec3, seed: u64) -> BlockData {
let offset = chunk_position * CHUNK_SIZE as i32;
let mut cave_noise = FastNoise::seeded(seed);
cave_noise.set_fractal_type(FractalType::FBM);
cave_noise.set_frequency(0.1);
let mut dirt_noise = FastNoise::seeded(seed.rotate_left(1));
dirt_noise.set_fractal_type(FractalType::FBM);
dirt_noise.set_frequency(0.1);
let mut blocks = Box::new([[[Block::Air; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]);
if chunk_position.y >= 0 {
if chunk_position.y == 0 {
for x in 0..CHUNK_SIZE {
for z in 0..CHUNK_SIZE {
blocks[x][0][z] = Block::Dirt;
blocks[x][1][z] = Block::Grass;
}
}
}
} else {
for x in 0..CHUNK_SIZE {
for y in 0..CHUNK_SIZE {
for z in 0..CHUNK_SIZE {
let position = ivec3(x as i32, y as i32, z as i32) + offset;
let v_cave_noise = cave_noise.get_noise3d(position.x as f32, position.y as f32, position.z as f32) * (-position.y as f32 - 10.0).clamp(0., 1.);
let v_dirt_noise = dirt_noise.get_noise3d(position.x as f32, position.y as f32, position.z as f32) * (-position.y as f32).clamp(0., 1.);
if v_cave_noise > 0.5 {
blocks[x][y][z] = Block::Stone;
} else if v_dirt_noise > 0.5 {
blocks[x][y][z] = Block::Dirt;
}
}
}
}
}
blocks
}