├── .gitignore ├── .travis.yml ├── .vscode └── tasks.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── appveyor-build.ps1 ├── appveyor-test.ps1 ├── appveyor.yml ├── clippy.toml ├── kaboom ├── Cargo.toml ├── debug_run_server.sh ├── run_server.sh ├── run_server_and_client.sh └── src │ ├── client_state.rs │ ├── death_system.rs │ ├── fighter.rs │ ├── game_state.rs │ ├── game_system.rs │ ├── health.rs │ ├── main.rs │ ├── message.rs │ ├── planet.rs │ ├── player.rs │ ├── recv_demux_system.rs │ ├── send_mux_system.rs │ └── weapon │ ├── explode_system.rs │ ├── grenade.rs │ ├── mod.rs │ ├── recv_system.rs │ └── shoot_system.rs ├── planetkit-grid ├── Cargo.toml └── src │ ├── cell_shape.rs │ ├── dir.rs │ ├── equivalent_points.rs │ ├── grid_point2.rs │ ├── grid_point3.rs │ ├── lib.rs │ ├── movement │ ├── mod.rs │ ├── step.rs │ ├── tests.rs │ ├── transform.rs │ ├── triangles.rs │ ├── turn.rs │ └── util.rs │ ├── neighbors.rs │ └── root.rs ├── planetkit.sublime-project ├── planetkit ├── Cargo.toml ├── build.rs ├── examples │ └── quick_start.rs └── src │ ├── app.rs │ ├── app_builder.rs │ ├── camera │ └── mod.rs │ ├── cell_dweller │ ├── cell_dweller.rs │ ├── mining.rs │ ├── mining_system.rs │ ├── mod.rs │ ├── movement_system.rs │ ├── physics_system.rs │ └── recv_system.rs │ ├── globe │ ├── chunk.rs │ ├── chunk_origin.rs │ ├── chunk_pair.rs │ ├── chunk_shared_points.rs │ ├── chunk_system.rs │ ├── chunk_view.rs │ ├── chunk_view_system.rs │ ├── cursor.rs │ ├── gen.rs │ ├── globe.rs │ ├── globe_ext.rs │ ├── icosahedron.rs │ ├── iters.rs │ ├── mod.rs │ ├── spec.rs │ ├── tests.rs │ └── view.rs │ ├── input_adapter.rs │ ├── integration_tests │ ├── mod.rs │ └── random_walk.rs │ ├── lib.rs │ ├── log_resource.rs │ ├── net │ ├── mod.rs │ ├── new_peer_system.rs │ ├── recv_system.rs │ ├── send_system.rs │ ├── server.rs │ ├── server_resource.rs │ ├── tcp.rs │ ├── tests.rs │ └── udp.rs │ ├── physics │ ├── collider.rs │ ├── gravity_system.rs │ ├── mass.rs │ ├── mod.rs │ ├── physics_system.rs │ ├── rigid_body.rs │ ├── velocity.rs │ └── velocity_system.rs │ ├── render │ ├── axes_mesh.rs │ ├── default_pipeline.rs │ ├── encoder_channel.rs │ ├── mesh.rs │ ├── mesh_repository.rs │ ├── mod.rs │ ├── proto_mesh.rs │ ├── system.rs │ └── visual.rs │ ├── shaders │ ├── copypasta_150.glslf │ ├── copypasta_150.glslv │ ├── copypasta_300_es.glslf │ └── copypasta_300_es.glslv │ ├── simple.rs │ ├── spatial.rs │ ├── types.rs │ └── window.rs ├── pre_commit.sh ├── screenshot.png ├── travis.sh ├── warm_build_cache.sh ├── webdemo ├── Cargo.lock ├── Cargo.toml ├── build.sh ├── index.html └── src │ └── main.rs └── woolgather ├── Cargo.toml └── src ├── game_state.rs ├── game_system.rs ├── main.rs └── shepherd.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.sublime-workspace 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | script: ./travis.sh 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "cargo", 4 | "isShellCommand": true, 5 | "showOutput": "always", 6 | "suppressTaskName": true, 7 | "tasks": [ 8 | { 9 | "taskName": "cargo build", 10 | "args": [ 11 | "build" 12 | ], 13 | "isBuildCommand": true 14 | }, 15 | { 16 | "taskName": "cargo run", 17 | "args": [ 18 | "run" 19 | ] 20 | }, 21 | { 22 | "taskName": "cargo test", 23 | "args": [ 24 | "test" 25 | ], 26 | "isTestCommand": true 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | # Position and direction types for geodesic grids 4 | "planetkit-grid", 5 | # Main PlanetKit crate 6 | "planetkit", 7 | # Example single-player game involving rescuing sheep that have fallen into holes 8 | "woolgather", 9 | # Example multi-player game involving shooting each other with explosive weapons 10 | "kaboom", 11 | ] 12 | 13 | exclude = [ 14 | # Demo of running parts of PlanetKit in a browser 15 | # Don't actually include in workspace. We don't want to accidentally try to build 16 | # it all the time on non-web targets. 17 | "webdemo", 18 | ] 19 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Jeff Parsons 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlanetKit 2 | 3 | [![Travis CI build status][bi]][bl] [![AppVeyor build status][ai]][al] 4 | 5 | [bi]: https://travis-ci.org/jeffparsons/planetkit.svg?branch=master 6 | [bl]: https://travis-ci.org/jeffparsons/planetkit 7 | 8 | [ai]: https://ci.appveyor.com/api/projects/status/vfk0w163ojw8nmdv/branch/master?svg=true 9 | [al]: https://ci.appveyor.com/project/jeffparsons/planetkit/branch/master 10 | 11 | 12 | Colorful blobs that might one day resemble planets. 13 | 14 | Requires the latest stable version of Rust. 15 | 16 | ![Screenshot](https://raw.githubusercontent.com/jeffparsons/planetkit/master/screenshot.png) 17 | 18 | 19 | ## Goals 20 | 21 | - **Build an easily-hackable toolkit** for building games based around voxel globes. The simple case should be simple, and the migration path to greater customisation should be smooth. 22 | 23 | - **Document everything**. Both the API and implementation should aim to teach. I'm also [blogging as I go](https://jeffparsons.github.io/). 24 | 25 | - **Be open and welcoming**. If you have a question, a suggestion, an idea, or just want to say "hi", please feel free to [open an issue](https://github.com/jeffparsons/planetkit/issues/new?title=Hi%20there!). 26 | 27 | 28 | ## Non-goals 29 | 30 | - **Build a game engine**. I'm going to end up doing this accidentally. But my preference is to use existing libraries where they exist, and contribute to them where it makes sense. 31 | 32 | - **Expose a stable API**. I do value stability, but PlanetKit is too young and exploratory to be thinking about that just yet. If you use it for something, I'll help you deal with breakage. 33 | 34 | - **Meet everyone's needs**. I intend to focus on a very narrow set of game styles. But "narrow" is hard to define clearly. If you're not sure whether our visions are aligned, please open an issue! 35 | 36 | 37 | ## License 38 | 39 | PlanetKit is primarily distributed under the terms of both the MIT license 40 | and the Apache License (Version 2.0). 41 | 42 | See LICENSE-APACHE and LICENSE-MIT for details. 43 | -------------------------------------------------------------------------------- /appveyor-build.ps1: -------------------------------------------------------------------------------- 1 | if ($env:CONFIGURATION -eq 'release') { 2 | # TODO: These "arguments" might get passed as a 3 | # single argument if there are multiple. Read up on 4 | # `Start-Process` etc. to figure out how to do this properly. 5 | $cargoargs = '--release' 6 | } 7 | cargo build --all $cargoargs 8 | -------------------------------------------------------------------------------- /appveyor-test.ps1: -------------------------------------------------------------------------------- 1 | if ($env:CONFIGURATION -eq 'release') { 2 | # TODO: These "arguments" might get passed as a 3 | # single argument if there are multiple. Read up on 4 | # `Start-Process` etc. to figure out how to do this properly. 5 | $cargoargs = '--release' 6 | } 7 | cargo test --all $cargoargs 8 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - TARGET: 1.27.0-x86_64-pc-windows 4 | COMPILER: msvc 5 | - TARGET: nightly-x86_64-pc-windows 6 | COMPILER: msvc 7 | configuration: 8 | - debug 9 | - release 10 | install: 11 | - ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-${env:TARGET}-${env:COMPILER}.exe" -FileName "rust-install.exe" 12 | - ps: .\rust-install.exe /VERYSILENT /NORESTART /DIR="C:\rust" | Out-Null 13 | - ps: $env:PATH="$env:PATH;C:\rust\bin" 14 | - rustc -vV 15 | - cargo -vV 16 | build_script: 17 | - ps: .\appveyor-build.ps1 18 | test_script: 19 | - ps: .\appveyor-test.ps1 20 | artifacts: 21 | - path: target/release/kaboom.exe 22 | name: Kaboom 23 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | doc-valid-idents = ["PlanetKit"] 2 | -------------------------------------------------------------------------------- /kaboom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kaboom" 3 | version = "0.0.1" 4 | authors = ["Jeff Parsons "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | planetkit = { path = "../planetkit" } 9 | rand = "0.6" 10 | rand_xoshiro = "0.1" 11 | shred = "0.7" 12 | specs = "0.14" 13 | slog = "2.0.4" 14 | serde = "1.0.10" 15 | serde_derive = "1.0.10" 16 | clap = "2.26.2" 17 | piston = "0.42" 18 | piston_window = "0.89" 19 | nalgebra = "0.18" 20 | ncollide3d = "0.19" 21 | nphysics3d = "0.11" 22 | -------------------------------------------------------------------------------- /kaboom/debug_run_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cargo run "$@" -- listen 6 | -------------------------------------------------------------------------------- /kaboom/run_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cargo run --release "$@" -- listen 6 | -------------------------------------------------------------------------------- /kaboom/run_server_and_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cargo run --release "$@" -- listen & 6 | sleep 1 7 | cargo run --release "$@" -- connect 127.0.0.1:62831 8 | -------------------------------------------------------------------------------- /kaboom/src/client_state.rs: -------------------------------------------------------------------------------- 1 | use specs::Entity; 2 | 3 | use crate::player::PlayerId; 4 | 5 | /// `World`-global resource for client-specific game state. 6 | #[derive(Default)] 7 | pub struct ClientState { 8 | // This might eventually become a list if, e.g., 9 | // we implement multiple players splitscreen on one client. 10 | pub player_id: Option, 11 | // we can unilaterally create the camera entity and 12 | // never tell other peers about it. 13 | pub camera_entity: Option, 14 | } 15 | -------------------------------------------------------------------------------- /kaboom/src/fighter.rs: -------------------------------------------------------------------------------- 1 | use specs; 2 | use specs::{Entities, LazyUpdate, Read}; 3 | 4 | use crate::pk; 5 | use crate::pk::cell_dweller; 6 | use crate::pk::globe::Globe; 7 | use crate::pk::grid; 8 | use crate::pk::render; 9 | use crate::pk::types::*; 10 | 11 | use crate::health::Health; 12 | use crate::player::PlayerId; 13 | 14 | pub struct Fighter { 15 | pub player_id: PlayerId, 16 | pub seconds_between_shots: TimeDelta, 17 | pub seconds_until_next_shot: TimeDelta, 18 | } 19 | 20 | impl Fighter { 21 | pub fn new(player_id: PlayerId) -> Fighter { 22 | Fighter { 23 | player_id: player_id, 24 | // TODO: accept as parameter 25 | seconds_between_shots: 0.5, 26 | seconds_until_next_shot: 0.0, 27 | } 28 | } 29 | } 30 | 31 | impl specs::Component for Fighter { 32 | // TODO: more appropriate storage 33 | type Storage = specs::VecStorage; 34 | } 35 | 36 | /// Create the player character. 37 | pub fn create( 38 | entities: &Entities<'_>, 39 | updater: &Read<'_, LazyUpdate>, 40 | globe_entity: specs::Entity, 41 | globe: &mut Globe, 42 | player_id: PlayerId, 43 | ) -> specs::Entity { 44 | use rand::SeedableRng; 45 | use rand_xoshiro::Xoshiro256StarStar; 46 | 47 | // Find a suitable spawn point for the player character at the globe surface. 48 | let (globe_spec, fighter_pos) = { 49 | let globe_spec = globe.spec(); 50 | // Seed spawn point RNG with world seed. 51 | let mut rng = Xoshiro256StarStar::from_seed(globe_spec.seed_as_u8_array); 52 | let fighter_pos = globe 53 | .air_above_random_surface_dry_land( 54 | &mut rng, 2, // Min air cells above 55 | 5, // Max distance from starting point 56 | 5, // Max attempts 57 | ) 58 | .expect("Oh noes, we took too many attempts to find a decent spawn point!"); 59 | (globe_spec, fighter_pos) 60 | }; 61 | 62 | // Make visual appearance of player character. 63 | // For now this is just an axes mesh. 64 | let mut fighter_visual = render::Visual::new_empty(); 65 | fighter_visual.proto_mesh = Some(render::make_axes_mesh()); 66 | 67 | let entity = entities.create(); 68 | updater.insert( 69 | entity, 70 | cell_dweller::CellDweller::new( 71 | fighter_pos, 72 | grid::Dir::default(), 73 | globe_spec, 74 | Some(globe_entity), 75 | ), 76 | ); 77 | updater.insert(entity, fighter_visual); 78 | // The CellDweller's transformation will be set based 79 | // on its coordinates in cell space. 80 | updater.insert(entity, pk::Spatial::new(globe_entity, Iso3::identity())); 81 | // Give the fighter some starting health. 82 | updater.insert(entity, Health::new(100)); 83 | updater.insert(entity, crate::fighter::Fighter::new(player_id)); 84 | entity 85 | } 86 | -------------------------------------------------------------------------------- /kaboom/src/game_state.rs: -------------------------------------------------------------------------------- 1 | use std::collections::vec_deque::VecDeque; 2 | 3 | use specs::Entity; 4 | 5 | use crate::player::{Player, PlayerId}; 6 | 7 | /// `World`-global resource for game state, 8 | /// largely defined by the server, but much also maintained 9 | /// by clients as they are informed about the state of the world. 10 | pub struct GameState { 11 | pub globe_entity: Option, 12 | // TODO: this should probably not be a Vec; 13 | // in practice we can be pretty sure clients will 14 | // hear about new players in order, but it's still not 15 | // the right kind of structure to store this in. 16 | // 17 | // TODO: is there any reason for players to not just 18 | // be another kind of component? They do hold a local 19 | // peer ID... but is that enough reason? 20 | // 21 | // NO GOOD REASON. TODO: make it a component. 22 | pub players: Vec, 23 | // New players that have joined but haven't been initialized. 24 | // Only the server cares about this. 25 | // (TODO: split it out into a ServerState or MasterState struct? 26 | // Maybe that's not worth it...) 27 | pub new_players: VecDeque, 28 | // Used for default player names when they are created. 29 | pub next_unnamed_player_number: usize, 30 | } 31 | 32 | impl Default for GameState { 33 | fn default() -> GameState { 34 | GameState { 35 | globe_entity: None, 36 | players: Vec::::new(), 37 | new_players: VecDeque::::new(), 38 | next_unnamed_player_number: 1, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /kaboom/src/health.rs: -------------------------------------------------------------------------------- 1 | use specs; 2 | 3 | use crate::player::PlayerId; 4 | 5 | /// Health points, which can be depleted by incurring damage. 6 | /// 7 | /// Health may go below zero if an damage causes greater than 8 | /// the remaining points. It is up to specific games whether to 9 | /// allow incurring further damage when health is already at or 10 | /// below zero. 11 | pub struct Health { 12 | pub hp: i32, 13 | pub last_damaged_by_player_id: Option, 14 | } 15 | 16 | impl Health { 17 | pub fn new(initial_hp: i32) -> Health { 18 | Health { 19 | hp: initial_hp, 20 | last_damaged_by_player_id: None, 21 | } 22 | } 23 | } 24 | 25 | impl specs::Component for Health { 26 | type Storage = specs::VecStorage; 27 | } 28 | -------------------------------------------------------------------------------- /kaboom/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate slog; 3 | #[macro_use] 4 | extern crate serde_derive; 5 | 6 | mod client_state; 7 | mod death_system; 8 | mod fighter; 9 | mod game_state; 10 | mod game_system; 11 | mod health; 12 | mod message; 13 | mod planet; 14 | mod player; 15 | mod recv_demux_system; 16 | mod send_mux_system; 17 | mod weapon; 18 | 19 | use std::sync::mpsc; 20 | 21 | use crate::message::Message; 22 | use crate::recv_demux_system::RecvDemuxSystem; 23 | use crate::send_mux_system::SendMuxSystem; 24 | use clap::{self, AppSettings, Arg, SubCommand}; 25 | use planetkit as pk; 26 | use specs; 27 | 28 | fn main() { 29 | let matches = clap::App::new("Kaboom") 30 | .author("Jeff Parsons ") 31 | .about("Blow stuff up!") 32 | .setting(AppSettings::SubcommandRequiredElseHelp) 33 | .subcommand( 34 | SubCommand::with_name("connect") 35 | .about("connect to a server") 36 | .arg( 37 | Arg::with_name("SERVER_ADDRESS") 38 | .help("The IP or hostname and port of the server to connect to") 39 | .required(true) 40 | .index(1), 41 | ), 42 | ) 43 | .subcommand(SubCommand::with_name("listen").about("start a server, and play")) 44 | // TODO: dedicated server, and helper script 45 | // to launch a dedicated server then connect 46 | // a client to it. 47 | .get_matches(); 48 | 49 | // Set up input adapters. 50 | let (shoot_input_sender, shoot_input_receiver) = mpsc::channel(); 51 | let shoot_input_adapter = Box::new(weapon::ShootInputAdapter::new(shoot_input_sender)); 52 | 53 | let mut app = pk::AppBuilder::new() 54 | .with_networking::() 55 | .with_common_systems() 56 | .with_systems( 57 | |logger: &slog::Logger, 58 | world: &mut specs::World, 59 | dispatcher_builder: specs::DispatcherBuilder<'static, 'static>| { 60 | add_systems(logger, world, dispatcher_builder, shoot_input_receiver) 61 | }, 62 | ) 63 | .build_gui(); 64 | 65 | app.add_input_adapter(shoot_input_adapter); 66 | 67 | // Should we start a server or connect to one? 68 | // NLL SVP. 69 | { 70 | use crate::pk::net::ServerResource; 71 | use piston_window::AdvancedWindow; 72 | use std::net::SocketAddr; 73 | 74 | // Systems we added will have ensured ServerResource is present. 75 | let (world, window) = app.world_and_window_mut(); 76 | let server_resource = world.write_resource::>(); 77 | let mut server = server_resource 78 | .server 79 | .lock() 80 | .expect("Failed to lock server"); 81 | if let Some(_matches) = matches.subcommand_matches("listen") { 82 | window.set_title("Kaboom (server)".to_string()); 83 | // TODO: make port configurable 84 | server.start_listen(62831); 85 | 86 | // Let the game know it's in charge of the world. 87 | let mut node_resource = world.write_resource::(); 88 | node_resource.is_master = true; 89 | } else if let Some(matches) = matches.subcommand_matches("connect") { 90 | window.set_title("Kaboom (client)".to_string()); 91 | // TODO: make port configurable 92 | let connect_addr = matches.value_of("SERVER_ADDRESS").unwrap(); 93 | let connect_addr: SocketAddr = connect_addr.parse().expect("Invalid SERVER_ADDRESS"); 94 | server.connect(connect_addr); 95 | } 96 | } 97 | 98 | app.run(); 99 | } 100 | 101 | fn add_systems( 102 | logger: &slog::Logger, 103 | world: &mut specs::World, 104 | dispatcher_builder: specs::DispatcherBuilder<'static, 'static>, 105 | shoot_input_receiver: mpsc::Receiver, 106 | ) -> specs::DispatcherBuilder<'static, 'static> { 107 | // TODO: systems should register these. 108 | world.register::(); 109 | world.register::(); 110 | world.register::(); 111 | 112 | let game_system = game_system::GameSystem::new(logger); 113 | let new_peer_system = pk::net::NewPeerSystem::::new(logger, world); 114 | let recv_system = pk::net::RecvSystem::::new(logger, world); 115 | let recv_demux_system = RecvDemuxSystem::new(logger, world); 116 | let cd_recv_system = pk::cell_dweller::RecvSystem::new(logger); 117 | let weapon_recv_system = weapon::RecvSystem::new(logger); 118 | let shoot_system = weapon::ShootSystem::new(shoot_input_receiver, logger); 119 | let explode_system = weapon::ExplodeSystem::new(logger); 120 | let death_system = death_system::DeathSystem::new(logger); 121 | let velocity_system = pk::physics::VelocitySystem::new(logger); 122 | let gravity_system = pk::physics::GravitySystem::new(logger); 123 | let physics_system = pk::physics::PhysicsSystem::new(); 124 | let send_mux_system = SendMuxSystem::new(logger); 125 | let send_system = pk::net::SendSystem::::new(logger, world); 126 | 127 | // TODO: these barriers are probably a bad idea; 128 | // we should be perfectly happy to render while we're sending 129 | // things over the network. Maybe consider "dummy systems" 130 | // used as lifecycle hooks instead. 131 | dispatcher_builder 132 | .with(game_system, "kaboom_game", &[]) 133 | .with(new_peer_system, "new_peer_system", &[]) 134 | .with(recv_system, "net_recv", &[]) 135 | .with(recv_demux_system, "recv_demux", &["net_recv"]) 136 | .with_barrier() 137 | .with(cd_recv_system, "cd_recv", &[]) 138 | .with(weapon_recv_system, "weapon_recv", &[]) 139 | .with(shoot_system, "shoot_grenade", &[]) 140 | .with(explode_system, "explode_grenade", &[]) 141 | .with(death_system, "death", &[]) 142 | .with(gravity_system, "gravity", &[]) 143 | .with(velocity_system, "velocity", &["gravity"]) 144 | // TODO: move gravity into nphysics as a force. 145 | .with(physics_system, "physics", &["gravity"]) 146 | // TODO: explicitly add all systems here, 147 | // instead of whatever "simple" wants to throw at you. 148 | // At the moment they might execute in an order that 149 | // could add unnecessary latency to receiving/sending messages. 150 | .with_barrier() 151 | .with(send_mux_system, "send_mux", &[]) 152 | .with(send_system, "net_send", &["send_mux"]) 153 | } 154 | -------------------------------------------------------------------------------- /kaboom/src/message.rs: -------------------------------------------------------------------------------- 1 | use crate::pk::net::GameMessage; 2 | 3 | use crate::pk::cell_dweller::CellDwellerMessage; 4 | 5 | use crate::player::PlayerMessage; 6 | 7 | use crate::weapon::WeaponMessage; 8 | 9 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 10 | pub enum Message { 11 | CellDweller(CellDwellerMessage), 12 | Player(PlayerMessage), 13 | Weapon(WeaponMessage), 14 | } 15 | impl GameMessage for Message {} 16 | -------------------------------------------------------------------------------- /kaboom/src/planet.rs: -------------------------------------------------------------------------------- 1 | use specs; 2 | use specs::{Entities, LazyUpdate, Read}; 3 | 4 | use crate::pk; 5 | use crate::pk::globe::{Globe, Spec}; 6 | 7 | // Create a planet to fight on. 8 | pub fn create(entities: &Entities<'_>, updater: &Read<'_, LazyUpdate>) -> specs::Entity { 9 | // Make it small enough that you can find another person easily enough. 10 | // TODO: eventually make it scale to the number of players present at the start of each round. 11 | // TODO: special generator for this; you want to have lava beneath the land 12 | let ocean_radius = 30.0; 13 | let crust_depth = 25.0; 14 | let floor_radius = ocean_radius - crust_depth; 15 | let spec = Spec::new( 16 | // TODO: random seed every time. 17 | 14, 18 | floor_radius, 19 | ocean_radius, 20 | 0.65, 21 | // TODO: calculate this (experimentally if necessary) based on the size of the blocks you want 22 | [64, 128], 23 | // Chunks should probably be taller, but short chunks are a bit 24 | // better for now in exposing bugs visually. 25 | [16, 16, 4], 26 | ); 27 | let globe = Globe::new(spec); 28 | 29 | let entity = entities.create(); 30 | updater.insert(entity, globe); 31 | updater.insert(entity, pk::Spatial::new_root()); 32 | entity 33 | } 34 | -------------------------------------------------------------------------------- /kaboom/src/player.rs: -------------------------------------------------------------------------------- 1 | use std::collections::vec_deque::VecDeque; 2 | 3 | use specs; 4 | 5 | use crate::pk::net::{PeerId, RecvMessage}; 6 | 7 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Copy)] 8 | pub struct PlayerId(pub u16); 9 | 10 | pub struct Player { 11 | pub id: PlayerId, 12 | // There could be many players per network peer, 13 | // e.g., if we ever get around to adding splitscreen. 14 | // 15 | // TODO: does this need to be a local peer ID? 16 | // I don't see any reason this can't be a global one. 17 | // E.g. node_id. Then we don't need to do any swizzling when 18 | // transferring these entities around -- everyone gets a global ID. 19 | // 20 | // TODO: definitely revisit this once `specs::saveload` is released (0.11?) 21 | // and you can start using that. 22 | pub peer_id: PeerId, 23 | pub fighter_entity: Option, 24 | pub name: String, 25 | pub points: i64, 26 | } 27 | 28 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 29 | pub enum PlayerMessage { 30 | NewPlayer(NewPlayerMessage), 31 | // Tell a client about the new player ID created for them, 32 | // or the player they are taking over. 33 | YourPlayer(PlayerId), 34 | NewFighter(u64, PlayerId), 35 | YourFighter(u64), 36 | } 37 | 38 | // REVISIT: just serialize an entire player instead, 39 | // once everything in it is global? Only if there's 40 | // no privileged information in it. 41 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 42 | pub struct NewPlayerMessage { 43 | pub id: PlayerId, 44 | pub name: String, 45 | } 46 | 47 | /// `World`-global resource for inbound player-related network messages. 48 | #[derive(Default)] 49 | pub struct RecvMessageQueue { 50 | pub queue: VecDeque>, 51 | } 52 | -------------------------------------------------------------------------------- /kaboom/src/recv_demux_system.rs: -------------------------------------------------------------------------------- 1 | use slog::Logger; 2 | use specs; 3 | use specs::Write; 4 | 5 | use crate::pk::cell_dweller; 6 | use crate::pk::net::{RecvMessage, RecvMessageQueue}; 7 | 8 | use crate::message::Message; 9 | use crate::player; 10 | use crate::weapon; 11 | 12 | pub struct RecvDemuxSystem { 13 | log: Logger, 14 | } 15 | 16 | impl RecvDemuxSystem { 17 | pub fn new(parent_log: &Logger, _world: &mut specs::World) -> RecvDemuxSystem { 18 | RecvDemuxSystem { 19 | log: parent_log.new(o!()), 20 | } 21 | } 22 | } 23 | 24 | impl<'a> specs::System<'a> for RecvDemuxSystem { 25 | type SystemData = ( 26 | Write<'a, RecvMessageQueue>, 27 | Write<'a, cell_dweller::RecvMessageQueue>, 28 | Write<'a, player::RecvMessageQueue>, 29 | Write<'a, weapon::RecvMessageQueue>, 30 | ); 31 | 32 | fn run(&mut self, data: Self::SystemData) { 33 | let ( 34 | mut recv_message_queue, 35 | mut cell_dweller_recv_queue, 36 | mut player_recv_queue, 37 | mut weapon_recv_queue, 38 | ) = data; 39 | 40 | // Drain the recv message queue, and dispatch to system-specific queues. 41 | while let Some(message) = recv_message_queue.queue.pop_front() { 42 | match message.game_message { 43 | Message::CellDweller(cd_message) => { 44 | trace!(self.log, "Forwarding cell dweller message to its recv message queue"; "message" => format!("{:?}", cd_message)); 45 | cell_dweller_recv_queue.queue.push_back(RecvMessage { 46 | source: message.source, 47 | game_message: cd_message, 48 | }); 49 | } 50 | Message::Player(player_message) => { 51 | trace!(self.log, "Forwarding player message to its recv message queue"; "message" => format!("{:?}", player_message)); 52 | player_recv_queue.queue.push_back(RecvMessage { 53 | source: message.source, 54 | game_message: player_message, 55 | }); 56 | } 57 | Message::Weapon(weapon_message) => { 58 | trace!(self.log, "Forwarding weapon message to its recv message queue"; "message" => format!("{:?}", weapon_message)); 59 | weapon_recv_queue.queue.push_back(RecvMessage { 60 | source: message.source, 61 | game_message: weapon_message, 62 | }); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /kaboom/src/send_mux_system.rs: -------------------------------------------------------------------------------- 1 | use slog::Logger; 2 | use specs; 3 | use specs::Write; 4 | 5 | use crate::pk::cell_dweller; 6 | use crate::pk::net::{SendMessage, SendMessageQueue}; 7 | 8 | use crate::message::Message; 9 | 10 | pub struct SendMuxSystem { 11 | log: Logger, 12 | initialized: bool, 13 | } 14 | 15 | impl SendMuxSystem { 16 | pub fn new(parent_log: &Logger) -> SendMuxSystem { 17 | SendMuxSystem { 18 | log: parent_log.new(o!()), 19 | initialized: false, 20 | } 21 | } 22 | } 23 | 24 | impl<'a> specs::System<'a> for SendMuxSystem { 25 | type SystemData = ( 26 | Write<'a, SendMessageQueue>, 27 | Write<'a, cell_dweller::SendMessageQueue>, 28 | ); 29 | 30 | fn run(&mut self, data: Self::SystemData) { 31 | let (mut send_message_queue, mut cell_dweller_send_queue) = data; 32 | 33 | if !self.initialized { 34 | // Signal to CellDweller module that we want it 35 | // to publish network messages. 36 | // TODO: Use shrev instead of this stuff. 37 | cell_dweller_send_queue.has_consumer = true; 38 | 39 | self.initialized = true; 40 | } 41 | 42 | // Drain the cell_dweller queue into the send_message queue. 43 | while let Some(message) = cell_dweller_send_queue.queue.pop_front() { 44 | trace!(self.log, "Forwarding cell dweller message to send message queue"; "message" => format!("{:?}", message)); 45 | send_message_queue.queue.push_back(SendMessage { 46 | destination: message.destination, 47 | game_message: Message::CellDweller(message.game_message), 48 | transport: message.transport, 49 | }); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /kaboom/src/weapon/explode_system.rs: -------------------------------------------------------------------------------- 1 | use slog::Logger; 2 | use specs; 3 | use specs::{Entities, Read, ReadStorage, Write, WriteStorage}; 4 | 5 | use crate::pk::net::NodeResource; 6 | use crate::pk::physics; 7 | use crate::pk::types::*; 8 | use crate::pk::Spatial; 9 | 10 | use super::grenade::Grenade; 11 | use crate::health::Health; 12 | 13 | pub struct ExplodeSystem { 14 | log: Logger, 15 | } 16 | 17 | impl ExplodeSystem { 18 | pub fn new(parent_log: &Logger) -> ExplodeSystem { 19 | ExplodeSystem { 20 | log: parent_log.new(o!()), 21 | } 22 | } 23 | } 24 | 25 | impl<'a> specs::System<'a> for ExplodeSystem { 26 | type SystemData = ( 27 | Read<'a, TimeDeltaResource>, 28 | Entities<'a>, 29 | WriteStorage<'a, Grenade>, 30 | WriteStorage<'a, Health>, 31 | ReadStorage<'a, Spatial>, 32 | Read<'a, NodeResource>, 33 | Read<'a, physics::WorldResource>, 34 | ReadStorage<'a, physics::RigidBody>, 35 | ReadStorage<'a, physics::Collider>, 36 | Write<'a, physics::RemoveBodyQueue>, 37 | Write<'a, physics::RemoveColliderQueue>, 38 | ); 39 | 40 | fn run(&mut self, data: Self::SystemData) { 41 | use crate::pk::SpatialStorage; 42 | use specs::Join; 43 | 44 | let ( 45 | dt, 46 | entities, 47 | mut grenades, 48 | mut healths, 49 | spatials, 50 | node_resource, 51 | world_resource, 52 | rigid_bodies, 53 | colliders, 54 | mut remove_body_queue_resource, 55 | mut remove_collider_queue_resource, 56 | ) = data; 57 | 58 | let nphysics_world = &world_resource.world; 59 | let remove_body_queue = &mut remove_body_queue_resource.queue; 60 | let remove_collider_queue = &mut remove_collider_queue_resource.queue; 61 | 62 | for (grenade_entity, grenade, rigid_body, collider) in 63 | (&*entities, &mut grenades, &rigid_bodies, &colliders).join() 64 | { 65 | // Count down each grenade's timer, and remove it if 66 | // it's been alive too long. 67 | grenade.time_to_live_seconds -= dt.0; 68 | grenade.time_lived_seconds += dt.0; 69 | 70 | // Check if the grenade had collided with anything. 71 | // TODO: at the time of writing, we're not actually 72 | // using nphysics to apply velocity, but just updating 73 | // the grenade's position from planetkit's side. 74 | // This means that it could skip through the chunk 75 | // without touching it if it goes fast enough. 😅 76 | // Fix ASAP after testing initial hacks. :) 77 | // 78 | // Give a little grace period after firing, becase we 79 | // currently fire the grenade from the player's feet, 80 | // and we don't want it to immediately explode on the 81 | // ground beneath them. Even if we fix it to fire higher, 82 | // we probably don't want it to explode on the firing player, 83 | // so we'll need to do something a bit more subtle than this 84 | // eventually! 85 | // 86 | // TODO: Replace with sensor... you don't want this 87 | // thing to bounce... or DO you? :P Maybe that'll be 88 | // a setting: number of bounces. It'll be way more fun like that. 89 | use ncollide3d::events::ContactEvent; 90 | let did_hit_something = grenade.time_lived_seconds > 0.2 91 | && nphysics_world.contact_events().iter().any(|contact_event| { 92 | match contact_event { 93 | ContactEvent::Started(a, b) => { 94 | // Collision could be either way around...? 95 | *a == collider.collider_handle || *b == collider.collider_handle 96 | } 97 | _ => false, 98 | } 99 | }); 100 | 101 | if did_hit_something || grenade.time_to_live_seconds <= 0.0 { 102 | debug!(self.log, "Kaboom!"; "did_hit_something" => did_hit_something); 103 | 104 | entities 105 | .delete(grenade_entity) 106 | .expect("Wrong entity generation!"); 107 | 108 | // Queue it for removal from physics world. 109 | // 110 | // TODO: These are hacks until Specs addresses reading 111 | // the data of removed components. (Presumably some extension 112 | // to the existing FlaggedStorage where you indicate that 113 | // you want the channel to carry full component data 114 | // with each event?) 115 | // 116 | // See . 117 | use crate::pk::physics::{RemoveBodyMessage, RemoveColliderMessage}; 118 | remove_body_queue.push_back(RemoveBodyMessage { 119 | handle: rigid_body.body_handle, 120 | }); 121 | remove_collider_queue.push_back(RemoveColliderMessage { 122 | handle: collider.collider_handle, 123 | }); 124 | 125 | // NOTE: Hacks until we have saveload and figure out how to do networking better. 126 | if !node_resource.is_master { 127 | continue; 128 | } 129 | 130 | // Damage anything nearby that can take damage. 131 | for (living_thing_entity, health, _spatial) in 132 | (&*entities, &mut healths, &spatials).join() 133 | { 134 | let relative_transform = 135 | spatials.a_relative_to_b(living_thing_entity, grenade_entity); 136 | let distance_squared = relative_transform.translation.vector.norm_squared(); 137 | let blast_radius_squared = 2.5 * 2.5; 138 | 139 | if distance_squared <= blast_radius_squared { 140 | health.hp -= 100; 141 | health.last_damaged_by_player_id = Some(grenade.fired_by_player_id); 142 | debug!(self.log, "Damaged something!"); 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /kaboom/src/weapon/grenade.rs: -------------------------------------------------------------------------------- 1 | use ncollide3d::shape::ShapeHandle; 2 | use slog::Logger; 3 | use specs::{self, Entities, Entity, LazyUpdate, Read, ReadStorage, Write}; 4 | 5 | use crate::pk::cell_dweller::CellDweller; 6 | use crate::pk::physics::Mass; 7 | use crate::pk::physics::Velocity; 8 | use crate::pk::physics::{Collider, RigidBody, WorldResource}; 9 | use crate::pk::render; 10 | use crate::pk::types::*; 11 | use crate::pk::Spatial; 12 | 13 | use crate::player::PlayerId; 14 | 15 | pub struct Grenade { 16 | pub time_to_live_seconds: f64, 17 | // So that we don't accidentally make it explode 18 | // immediately after launching. This is a bit of a hack 19 | // because we launch it from the player's feet at the moment. 20 | // 21 | // But we probably want to support this in some form. 22 | // I'm thinking being able to set the max number of bounces 23 | // before you fire (like setting the grenade timer in Worms), 24 | // and have it ignore bounces that happen in rapid succession, 25 | // or immediately after firing. 26 | pub time_lived_seconds: f64, 27 | pub fired_by_player_id: PlayerId, 28 | } 29 | 30 | impl Grenade { 31 | pub fn new(fired_by_player_id: PlayerId) -> Grenade { 32 | Grenade { 33 | // It should usually hit terrain before running out 34 | // of time. But if you fire from a tall hill, 35 | // it might explode in the air. 36 | time_to_live_seconds: 3.0, 37 | time_lived_seconds: 0.0, 38 | fired_by_player_id: fired_by_player_id, 39 | } 40 | } 41 | } 42 | 43 | impl specs::Component for Grenade { 44 | type Storage = specs::HashMapStorage; 45 | } 46 | 47 | /// Spawn a grenade travelling up and forward away from the player. 48 | pub fn shoot_grenade( 49 | entities: &Entities<'_>, 50 | updater: &Read<'_, LazyUpdate>, 51 | cell_dwellers: &ReadStorage<'_, CellDweller>, 52 | cell_dweller_entity: Entity, 53 | spatials: &ReadStorage<'_, Spatial>, 54 | log: &Logger, 55 | fired_by_player_id: PlayerId, 56 | world_resource: &mut Write<'_, WorldResource>, 57 | ) { 58 | // Make visual appearance of bullet. 59 | // For now this is just an axes mesh. 60 | let mut bullet_visual = render::Visual::new_empty(); 61 | bullet_visual.proto_mesh = Some(render::make_axes_mesh()); 62 | 63 | let cd = cell_dwellers 64 | .get(cell_dweller_entity) 65 | .expect("Someone deleted the controlled entity's CellDweller"); 66 | let cd_spatial = spatials 67 | .get(cell_dweller_entity) 68 | .expect("Someone deleted the controlled entity's Spatial"); 69 | // Get the associated globe entity, complaining loudly if we fail. 70 | let globe_entity = match cd.globe_entity { 71 | Some(globe_entity) => globe_entity, 72 | None => { 73 | warn!( 74 | log, 75 | "There was no associated globe entity or it wasn't actually a Globe! Can't proceed!" 76 | ); 77 | return; 78 | } 79 | }; 80 | // Put bullet where player is. 81 | let bullet_spatial = Spatial::new(globe_entity, cd_spatial.local_transform()); 82 | 83 | // Shoot the bullet slightly up and away from us. 84 | // 85 | // (TODO: turn panic into error log.) 86 | // 87 | // NOTE: the `unwrap` here is not the normal meaning of unwrap; 88 | // in this case it is a totally innocuous function for extracting 89 | // the interior value of a unit vector. 90 | let dir = &cd_spatial.local_transform().rotation; 91 | let cd_relative_velocity = (Vec3::z_axis().into_inner() + Vec3::y_axis().into_inner()) * 7.0; 92 | let bullet_velocity = Velocity::new(dir * cd_relative_velocity); 93 | 94 | // Add a small ball for the grenade to the physics world. 95 | use ncollide3d::shape::Ball; 96 | use nphysics3d::object::{ColliderDesc, RigidBodyDesc}; 97 | let ball = Ball::::new(0.1); 98 | let ball_handle = ShapeHandle::new(ball); 99 | let world = &mut world_resource.world; 100 | 101 | // Set up rigid body. 102 | let collider_desc = ColliderDesc::new(ball_handle); 103 | let rigid_body_handle = RigidBodyDesc::new() 104 | .collider(&collider_desc) 105 | .position(cd_spatial.local_transform()) 106 | .build(world) 107 | .handle(); 108 | 109 | // Get a handle to the collider that was created 110 | // for the grenade. 111 | let collider_handle = world 112 | .collider_world() 113 | .body_colliders(rigid_body_handle) 114 | .next() 115 | .expect("There should be exactly one associated collider") 116 | .handle(); 117 | 118 | // Build the entity. 119 | let entity = entities.create(); 120 | updater.insert(entity, bullet_visual); 121 | updater.insert(entity, bullet_spatial); 122 | updater.insert(entity, bullet_velocity); 123 | updater.insert(entity, Mass {}); 124 | updater.insert(entity, Grenade::new(fired_by_player_id)); 125 | updater.insert(entity, Collider::new(collider_handle)); 126 | updater.insert(entity, RigidBody::new(rigid_body_handle)); 127 | } 128 | -------------------------------------------------------------------------------- /kaboom/src/weapon/mod.rs: -------------------------------------------------------------------------------- 1 | mod explode_system; 2 | mod grenade; 3 | mod recv_system; 4 | mod shoot_system; 5 | 6 | pub use self::explode_system::ExplodeSystem; 7 | pub use self::grenade::Grenade; 8 | pub use self::recv_system::RecvSystem; 9 | pub use self::shoot_system::ShootEvent; 10 | pub use self::shoot_system::ShootInputAdapter; 11 | pub use self::shoot_system::ShootSystem; 12 | 13 | use std::collections::vec_deque::VecDeque; 14 | 15 | use crate::pk::net::RecvMessage; 16 | 17 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 18 | pub enum WeaponMessage { 19 | ShootGrenade(ShootGrenadeMessage), 20 | NewGrenade(NewGrenadeMessage), 21 | // TODO: Can this become a generic when the specs 22 | // release with 'saveload' comes along? 23 | // DeleteGrenade(...), 24 | } 25 | 26 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 27 | pub struct ShootGrenadeMessage { 28 | fired_by_player_id: crate::player::PlayerId, 29 | fired_by_cell_dweller_entity_id: u64, 30 | } 31 | 32 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 33 | pub struct NewGrenadeMessage { 34 | fired_by_player_id: crate::player::PlayerId, 35 | fired_by_cell_dweller_entity_id: u64, 36 | } 37 | 38 | /// `World`-global resource for inbound weapon-related network messages. 39 | #[derive(Default)] 40 | pub struct RecvMessageQueue { 41 | pub queue: VecDeque>, 42 | } 43 | -------------------------------------------------------------------------------- /kaboom/src/weapon/recv_system.rs: -------------------------------------------------------------------------------- 1 | use slog::Logger; 2 | use specs; 3 | use specs::{Entities, LazyUpdate, Read, ReadStorage, Write}; 4 | 5 | use crate::pk::cell_dweller::CellDweller; 6 | use crate::pk::net::{Destination, EntityIds, SendMessage, SendMessageQueue, Transport}; 7 | use crate::pk::physics::WorldResource; 8 | use crate::pk::Spatial; 9 | 10 | use super::grenade::shoot_grenade; 11 | use super::RecvMessageQueue; 12 | use super::{NewGrenadeMessage, WeaponMessage}; 13 | use crate::message::Message; 14 | 15 | pub struct RecvSystem { 16 | log: Logger, 17 | } 18 | 19 | impl RecvSystem { 20 | pub fn new(parent_log: &Logger) -> RecvSystem { 21 | RecvSystem { 22 | log: parent_log.new(o!("system" => "weapon_recv")), 23 | } 24 | } 25 | } 26 | 27 | impl<'a> specs::System<'a> for RecvSystem { 28 | type SystemData = ( 29 | Write<'a, RecvMessageQueue>, 30 | Write<'a, SendMessageQueue>, 31 | Entities<'a>, 32 | Read<'a, LazyUpdate>, 33 | ReadStorage<'a, Spatial>, 34 | ReadStorage<'a, CellDweller>, 35 | Read<'a, EntityIds>, 36 | Write<'a, WorldResource>, 37 | ); 38 | 39 | fn run(&mut self, data: Self::SystemData) { 40 | let ( 41 | mut recv_message_queue, 42 | mut send_message_queue, 43 | entities, 44 | updater, 45 | spatials, 46 | cell_dwellers, 47 | entity_ids, 48 | mut world_resource, 49 | ) = data; 50 | 51 | while let Some(message) = recv_message_queue.queue.pop_front() { 52 | match message.game_message { 53 | WeaponMessage::ShootGrenade(shoot_grenade_message) => { 54 | // TODO: verify that we're the master 55 | 56 | trace!(self.log, "Firing grenade because a peer asked me to"; "message" => format!("{:?}", shoot_grenade_message)); 57 | 58 | // NOTE: Hacks until we have saveload; 59 | // just tell everyone including ourself to fire the grenade, 60 | // and then only the server will actually trigger an explosion 61 | // when the grenade runs out of time. 62 | // TODO: not this! 63 | send_message_queue.queue.push_back(SendMessage { 64 | destination: Destination::EveryoneIncludingSelf, 65 | game_message: Message::Weapon(WeaponMessage::NewGrenade( 66 | NewGrenadeMessage { 67 | fired_by_player_id: shoot_grenade_message.fired_by_player_id, 68 | fired_by_cell_dweller_entity_id: shoot_grenade_message 69 | .fired_by_cell_dweller_entity_id, 70 | }, 71 | )), 72 | // TODO: does it matter if we miss one — maybe UDP? 73 | // TCP for now, then solve this by having TTL on some entities. 74 | // Or a standard "TTL / clean-me-up" component type! :) 75 | transport: Transport::TCP, 76 | }); 77 | } 78 | WeaponMessage::NewGrenade(new_grenade_message) => { 79 | trace!(self.log, "Spawning grenade because server asked me to"; "message" => format!("{:?}", new_grenade_message)); 80 | 81 | // Look up the entity from its global ID. 82 | let cell_dweller_entity = match entity_ids 83 | .mapping 84 | .get(&new_grenade_message.fired_by_cell_dweller_entity_id) 85 | { 86 | Some(ent) => ent.clone(), 87 | // We probably just don't know about it yet. 88 | None => { 89 | debug!(self.log, "Unknown CellDweller fired a grenade"; "entity_id" => new_grenade_message.fired_by_cell_dweller_entity_id); 90 | continue; 91 | } 92 | }; 93 | 94 | shoot_grenade( 95 | &entities, 96 | &updater, 97 | &cell_dwellers, 98 | cell_dweller_entity, 99 | &spatials, 100 | &self.log, 101 | new_grenade_message.fired_by_player_id, 102 | &mut world_resource, 103 | ); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /kaboom/src/weapon/shoot_system.rs: -------------------------------------------------------------------------------- 1 | use piston::input::Input; 2 | use slog::Logger; 3 | use specs; 4 | use specs::{Read, ReadStorage, Write, WriteStorage}; 5 | use std::sync::mpsc; 6 | 7 | use crate::pk::cell_dweller::ActiveCellDweller; 8 | use crate::pk::input_adapter; 9 | use crate::pk::net::{Destination, NetMarker, SendMessage, SendMessageQueue, Transport}; 10 | use crate::pk::types::*; 11 | 12 | use super::{ShootGrenadeMessage, WeaponMessage}; 13 | use crate::client_state::ClientState; 14 | use crate::fighter::Fighter; 15 | use crate::message::Message; 16 | 17 | pub struct ShootInputAdapter { 18 | sender: mpsc::Sender, 19 | } 20 | 21 | impl ShootInputAdapter { 22 | pub fn new(sender: mpsc::Sender) -> ShootInputAdapter { 23 | ShootInputAdapter { sender: sender } 24 | } 25 | } 26 | 27 | impl input_adapter::InputAdapter for ShootInputAdapter { 28 | fn handle(&self, input_event: &Input) { 29 | use piston::input::keyboard::Key; 30 | use piston::input::{Button, ButtonState}; 31 | 32 | if let &Input::Button(button_args) = input_event { 33 | if let Button::Keyboard(key) = button_args.button { 34 | let is_down = match button_args.state { 35 | ButtonState::Press => true, 36 | ButtonState::Release => false, 37 | }; 38 | match key { 39 | Key::Space => self.sender.send(ShootEvent(is_down)).unwrap(), 40 | _ => (), 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | pub struct ShootEvent(bool); 48 | 49 | pub struct ShootSystem { 50 | input_receiver: mpsc::Receiver, 51 | log: Logger, 52 | shoot: bool, 53 | } 54 | 55 | impl ShootSystem { 56 | pub fn new(input_receiver: mpsc::Receiver, parent_log: &Logger) -> ShootSystem { 57 | ShootSystem { 58 | input_receiver: input_receiver, 59 | log: parent_log.new(o!()), 60 | shoot: false, 61 | } 62 | } 63 | 64 | fn consume_input(&mut self) { 65 | loop { 66 | match self.input_receiver.try_recv() { 67 | Ok(ShootEvent(b)) => self.shoot = b, 68 | Err(_) => return, 69 | } 70 | } 71 | } 72 | } 73 | 74 | impl<'a> specs::System<'a> for ShootSystem { 75 | type SystemData = ( 76 | Read<'a, TimeDeltaResource>, 77 | Read<'a, ActiveCellDweller>, 78 | WriteStorage<'a, Fighter>, 79 | Read<'a, ClientState>, 80 | Write<'a, SendMessageQueue>, 81 | ReadStorage<'a, NetMarker>, 82 | ); 83 | 84 | fn run(&mut self, data: Self::SystemData) { 85 | self.consume_input(); 86 | let ( 87 | dt, 88 | active_cell_dweller_resource, 89 | mut fighters, 90 | client_state, 91 | mut send_message_queue, 92 | net_markers, 93 | ) = data; 94 | 95 | // Find the active fighter, even if we're not currently trying to shoot; 96 | // we might need to count down the time until we can next shoot. 97 | // If there isn't one, then just silently move on. 98 | let active_cell_dweller_entity = match active_cell_dweller_resource.maybe_entity { 99 | Some(entity) => entity, 100 | None => return, 101 | }; 102 | 103 | if !fighters.get(active_cell_dweller_entity).is_some() { 104 | // This entity hasn't been realised yet; 105 | // can't do anything else with it this frame. 106 | // TODO: isn't this was `is_alive` is supposed to achieve? 107 | // And yet it doesn't seem to... 108 | return; 109 | } 110 | 111 | // Assume it is a fighter, because those are the only cell dwellers 112 | // you're allowed to control in this game. 113 | let active_fighter = fighters 114 | .get_mut(active_cell_dweller_entity) 115 | .expect("Cell dweller should have had a fighter attached!"); 116 | 117 | // Count down until we're allowed to shoot next. 118 | if active_fighter.seconds_until_next_shot > 0.0 { 119 | active_fighter.seconds_until_next_shot = 120 | (active_fighter.seconds_until_next_shot - dt.0).max(0.0); 121 | } 122 | let still_waiting_to_shoot = active_fighter.seconds_until_next_shot > 0.0; 123 | 124 | if self.shoot && !still_waiting_to_shoot { 125 | self.shoot = false; 126 | 127 | let fired_by_player_id = client_state 128 | .player_id 129 | .expect("There should be a current player."); 130 | let fired_by_cell_dweller_entity_id = net_markers 131 | .get(active_cell_dweller_entity) 132 | .expect("Active cell dweller should have global identity") 133 | .id; 134 | 135 | // Place the bullet in the same location as the player, 136 | // relative to the same globe. 137 | debug!(self.log, "Fire!"); 138 | 139 | // Ask the server/master to spawn a grenade. 140 | // (TODO: really need to decide on termonology around server/master/client/peer/etc.) 141 | send_message_queue.queue.push_back(SendMessage { 142 | destination: Destination::Master, 143 | game_message: Message::Weapon(WeaponMessage::ShootGrenade(ShootGrenadeMessage { 144 | fired_by_player_id: fired_by_player_id, 145 | fired_by_cell_dweller_entity_id: fired_by_cell_dweller_entity_id, 146 | })), 147 | transport: Transport::UDP, 148 | }); 149 | 150 | // Reset time until we can shoot again. 151 | active_fighter.seconds_until_next_shot = active_fighter.seconds_between_shots; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /planetkit-grid/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "planetkit-grid" 3 | version = "0.0.1" 4 | authors = ["Jeff Parsons "] 5 | license = "MIT/Apache-2.0" 6 | description = """ 7 | Types representing positions and directions on 2- and 3-dimensional geodesic grids. 8 | """ 9 | repository = "https://github.com/jeffparsons/planetkit" 10 | edition = "2018" 11 | 12 | [lib] 13 | path = "src/lib.rs" 14 | 15 | [dependencies] 16 | arrayvec = "0.4.5" 17 | num-traits = "0.2.0" 18 | nalgebra = "0.18" 19 | # TODO: Put this behind a non-default feature. 20 | rand = "0.6" 21 | # TODO: Put all serde-related things behind a default feature. 22 | serde = "1.0.10" 23 | serde_json = "1.0.2" 24 | serde_derive = "1.0.10" 25 | 26 | [dev-dependencies] 27 | itertools = "0.8.0" 28 | -------------------------------------------------------------------------------- /planetkit-grid/src/dir.rs: -------------------------------------------------------------------------------- 1 | pub type DirIndex = u8; 2 | 3 | #[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] 4 | pub struct Dir { 5 | pub index: DirIndex, 6 | } 7 | 8 | impl Dir { 9 | pub fn new(index: DirIndex) -> Dir { 10 | Dir { index } 11 | } 12 | 13 | /// Returns `true` if `self` points toward an immediately 14 | /// adjacent cell, or equivalently toward an edge of the 15 | /// current cell. 16 | /// 17 | /// Assumes this is in the context of a hexagonal cell -- 18 | /// i.e. not one of the 12 pentagons in the world. 19 | /// If you need to ask an equivalent question when you might 20 | /// be in a pentagonal cell, then first rebase your 21 | /// `(Pos, Dir)` onto a root quad that `self` points into, 22 | /// and then the relevant part of the current cell will 23 | /// then be equivalent to a hexagon for this purpose. 24 | pub fn points_at_hex_edge(self) -> bool { 25 | // On a hexagonal cell, any even direction index 26 | // points to an edge. 27 | self.index % 2 == 0 28 | } 29 | 30 | pub fn next_hex_edge_left(self) -> Dir { 31 | Dir::new((self.index + 2) % 12) 32 | } 33 | 34 | pub fn next_hex_edge_right(self) -> Dir { 35 | Dir::new((self.index + 12 - 2) % 12) 36 | } 37 | 38 | pub fn opposite(self) -> Dir { 39 | Dir::new((self.index + 6) % 12) 40 | } 41 | } 42 | 43 | impl From for Dir { 44 | fn from(dir_index: DirIndex) -> Dir { 45 | Dir::new(dir_index) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /planetkit-grid/src/grid_point2.rs: -------------------------------------------------------------------------------- 1 | use super::Point3; 2 | use super::{GridCoord, Root, RootIndex}; 3 | 4 | #[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)] 5 | pub struct Point2 { 6 | pub root: Root, 7 | pub x: GridCoord, 8 | pub y: GridCoord, 9 | } 10 | 11 | impl Point2 { 12 | pub fn new(root: Root, x: GridCoord, y: GridCoord) -> Point2 { 13 | Point2 { root, x, y } 14 | } 15 | 16 | pub fn with_root(&self, new_root_index: RootIndex) -> Self { 17 | let mut new_point = *self; 18 | new_point.root.index = new_root_index; 19 | new_point 20 | } 21 | 22 | pub fn with_x(&self, new_x: GridCoord) -> Self { 23 | let mut new_point = *self; 24 | new_point.x = new_x; 25 | new_point 26 | } 27 | 28 | pub fn with_y(&self, new_y: GridCoord) -> Self { 29 | let mut new_point = *self; 30 | new_point.y = new_y; 31 | new_point 32 | } 33 | 34 | pub fn with_z(&self, z: GridCoord) -> Point3 { 35 | Point3::new(self.root, self.x, self.y, z) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /planetkit-grid/src/grid_point3.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | use super::{GridCoord, Point2, Root, RootIndex}; 5 | 6 | #[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)] 7 | pub struct Point3 { 8 | pub rxy: Point2, 9 | pub z: GridCoord, 10 | } 11 | 12 | impl Point3 { 13 | pub fn new(root: Root, x: GridCoord, y: GridCoord, z: GridCoord) -> Point3 { 14 | Point3 { 15 | rxy: Point2::new(root, x, y), 16 | z, 17 | } 18 | } 19 | 20 | pub fn with_root(&self, new_root_index: RootIndex) -> Self { 21 | let mut new_point = *self; 22 | new_point.rxy.root.index = new_root_index; 23 | new_point 24 | } 25 | 26 | pub fn with_x(&self, new_x: GridCoord) -> Self { 27 | let mut new_point = *self; 28 | new_point.rxy.x = new_x; 29 | new_point 30 | } 31 | 32 | pub fn with_y(&self, new_y: GridCoord) -> Self { 33 | let mut new_point = *self; 34 | new_point.rxy.y = new_y; 35 | new_point 36 | } 37 | 38 | pub fn with_z(&self, new_z: GridCoord) -> Self { 39 | let mut new_point = *self; 40 | new_point.z = new_z; 41 | new_point 42 | } 43 | } 44 | 45 | /// Wrapper type around a `Point3` that is known to be expressed 46 | /// in its owning root quad. 47 | /// 48 | /// Note that this does not save you from accidentally using 49 | /// positions from multiple incompatible `Globe`s with different 50 | /// resolutions. 51 | #[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash)] 52 | pub struct PosInOwningRoot { 53 | pos: Point3, 54 | } 55 | 56 | impl Into for PosInOwningRoot { 57 | fn into(self) -> Point3 { 58 | self.pos 59 | } 60 | } 61 | 62 | impl PosInOwningRoot { 63 | // Returns a position equivalent to `pos`, 64 | // but in whatever root owns the data for `pos`. 65 | // 66 | // The output will only ever differ from the input 67 | // if `pos` is on the edge of a root quad. 68 | // 69 | // Behaviour is undefined (nonsense result or panic) 70 | // if `pos` lies beyond the edges of its root. 71 | pub fn new(pos: Point3, resolution: [GridCoord; 2]) -> PosInOwningRoot { 72 | debug_assert!(pos.z >= 0); 73 | 74 | // Here is the pattern of which root a cell belongs to. 75 | // 76 | // Note how adacent roots neatly slot into each other's 77 | // non-owned cells when wrapped around the globe. 78 | // 79 | // Also note the special cases for north and south poles; 80 | // they don't fit neatly into the general pattern. 81 | // 82 | // In the diagram below, each circle represents a hexagon 83 | // in a voxmap shell. Filled circles belong to the root, 84 | // and empty circles belong to an adjacent root. 85 | // 86 | // Root 0 Roots 1, 2, 3 Root 4 87 | // ------ ------------- ------ 88 | // 89 | // ● ◌ ◌ 90 | // ◌ ● ◌ ● ◌ ● 91 | // ◌ ● ● ◌ ● ● ◌ ● ● 92 | // ◌ ● ● ● ◌ ● ● ● ◌ ● ● ● 93 | // ◌ ● ● ● ● ◌ ● ● ● ● ◌ ● ● ● ● 94 | // ◌ ● ● ● ● ◌ ● ● ● ● ◌ ● ● ● ● 95 | // ◌ ● ● ● ● ◌ ● ● ● ● ◌ ● ● ● ● 96 | // ◌ ● ● ● ● ◌ ● ● ● ● ◌ ● ● ● ● 97 | // ◌ ● ● ● ● ◌ ● ● ● ● ◌ ● ● ● ● 98 | // ◌ ● ● ● ◌ ● ● ● ◌ ● ● ● 99 | // ◌ ● ● ◌ ● ● ◌ ● ● 100 | // ◌ ● ◌ ● ◌ ● 101 | // ◌ ◌ ● 102 | // 103 | let end_x = resolution[0]; 104 | let end_y = resolution[1]; 105 | let half_y = resolution[1] / 2; 106 | 107 | // Special cases for north and south poles 108 | let pos_in_owning_root = if pos.x == 0 && pos.y == 0 { 109 | // North pole 110 | Point3::new( 111 | // First root owns north pole. 112 | 0.into(), 113 | 0, 114 | 0, 115 | pos.z, 116 | ) 117 | } else if pos.x == end_x && pos.y == end_y { 118 | // South pole 119 | Point3::new( 120 | // Last root owns south pole. 121 | 4.into(), 122 | end_x, 123 | end_y, 124 | pos.z, 125 | ) 126 | } else if pos.y == 0 { 127 | // Roots don't own their north-west edge; 128 | // translate to next root's north-east edge. 129 | Point3::new(pos.root.next_west(), 0, pos.x, pos.z) 130 | } else if pos.x == end_x && pos.y < half_y { 131 | // Roots don't own their mid-west edge; 132 | // translate to the next root's mid-east edge. 133 | Point3::new(pos.root.next_west(), 0, half_y + pos.y, pos.z) 134 | } else if pos.x == end_x { 135 | // Roots don't own their south-west edge; 136 | // translate to the next root's south-east edge. 137 | Point3::new(pos.root.next_west(), pos.y - half_y, end_y, pos.z) 138 | } else { 139 | // `pos` is either on an edge owned by its root, 140 | // or somewhere in the middle of the root. 141 | pos 142 | }; 143 | 144 | PosInOwningRoot { 145 | pos: pos_in_owning_root, 146 | } 147 | } 148 | 149 | /// Set z-coordinate of underlying `Pos`. 150 | /// 151 | /// Note that this is the one safe axis to operate 152 | /// on without knowing the globe resolution. 153 | pub fn set_z(&mut self, new_z: GridCoord) { 154 | self.pos.z = new_z; 155 | } 156 | } 157 | 158 | impl<'a> PosInOwningRoot { 159 | pub fn pos(&'a self) -> &'a Point3 { 160 | &self.pos 161 | } 162 | } 163 | 164 | // Evil tricks to allow access to Point2 fields from `self.rxy` 165 | // as if they belong to `Self`. 166 | impl Deref for Point3 { 167 | type Target = Point2; 168 | 169 | fn deref(&self) -> &Point2 { 170 | &self.rxy 171 | } 172 | } 173 | 174 | impl DerefMut for Point3 { 175 | fn deref_mut(&mut self) -> &mut Point2 { 176 | &mut self.rxy 177 | } 178 | } 179 | 180 | /// Compare by root, z, y, then x. 181 | /// 182 | /// Sorting points by this will be cache-friendly when indexing into, 183 | /// e.g., `Chunk`s, which order their elements by z (coarsest) to x (finest). 184 | pub fn semi_arbitrary_compare(a: &Point3, b: &Point3) -> Ordering { 185 | let cmp_root = a.root.index.cmp(&b.root.index); 186 | if cmp_root != Ordering::Equal { 187 | return cmp_root; 188 | } 189 | let cmp_z = a.z.cmp(&b.z); 190 | if cmp_z != Ordering::Equal { 191 | return cmp_z; 192 | } 193 | let cmp_y = a.y.cmp(&b.y); 194 | if cmp_y != Ordering::Equal { 195 | return cmp_y; 196 | } 197 | a.x.cmp(&b.x) 198 | } 199 | -------------------------------------------------------------------------------- /planetkit-grid/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | #[cfg(test)] 4 | #[macro_use] 5 | extern crate itertools; 6 | 7 | use nalgebra as na; 8 | use rand::Rng; 9 | 10 | pub mod cell_shape; 11 | mod dir; 12 | mod equivalent_points; 13 | mod grid_point2; 14 | mod grid_point3; 15 | pub mod movement; 16 | mod neighbors; 17 | mod root; 18 | 19 | // TODO: be selective in what you export; no wildcards! 20 | pub use self::dir::*; 21 | pub use self::equivalent_points::*; 22 | pub use self::grid_point2::Point2; 23 | pub use self::grid_point3::*; 24 | pub use self::neighbors::*; 25 | pub use self::root::*; 26 | 27 | // TODO: Generic! 28 | pub type GridCoord = i64; 29 | 30 | /// Generate a random column on the globe. 31 | pub fn random_column(root_resolution: [GridCoord; 2], rng: &mut R) -> Point2 { 32 | // TODO: this is a bit dodgy; it isn't uniformly distributed 33 | // over all points in the world. You should just reject any points 34 | // that turn out not to be the canonical representation, 35 | // and try again. 36 | let root_index: RootIndex = rng.gen_range(0, 5); 37 | let x: GridCoord = rng.gen_range(0, root_resolution[0]); 38 | let y: GridCoord = rng.gen_range(0, root_resolution[0]); 39 | Point2::new(root_index.into(), x, y) 40 | } 41 | -------------------------------------------------------------------------------- /planetkit-grid/src/movement/mod.rs: -------------------------------------------------------------------------------- 1 | // See `triangles.rs` for discussion about the approach used 2 | // throughout the module, and the list of all triangles used. 3 | 4 | mod step; 5 | mod transform; 6 | mod triangles; 7 | mod turn; 8 | mod util; 9 | 10 | #[cfg(test)] 11 | mod tests; 12 | 13 | // TODO: figure out how to encourage use of the "good" functions, 14 | // while still exposing the "raw" ones for people who really want them. 15 | // Consider something like session types. 16 | 17 | pub use self::step::{ 18 | move_forward, step_backward_and_face_neighbor, step_forward_and_face_neighbor, 19 | }; 20 | pub use self::turn::{ 21 | turn_around_and_face_neighbor, turn_by_one_hex_edge, turn_left_by_one_hex_edge, 22 | turn_right_by_one_hex_edge, TurnDir, 23 | }; 24 | pub use self::util::{adjacent_pos_in_dir, is_pentagon}; 25 | -------------------------------------------------------------------------------- /planetkit-grid/src/movement/transform.rs: -------------------------------------------------------------------------------- 1 | /// These functions are used so that most movement calculations can assume we're in the 2 | /// arctic triangle of root 0 (see `triangles.rs`) to minimise the amount of 3 | /// special case logic. 4 | use crate::na; 5 | 6 | use super::triangles::*; 7 | use crate::cell_shape::NEIGHBOR_OFFSETS; 8 | use crate::{Dir, GridCoord, Point3}; 9 | 10 | // Use nalgebra for some local transformations. 11 | // We are ignoring z-axis completely because this kid of movement 12 | // is only in (x, y). 13 | type Pos2 = na::Point2; 14 | type PosMat2 = na::Matrix2; 15 | 16 | /// Transform `pos` and `dir` as specified relative to a given triangles apex, 17 | /// to be relative to the world, or equivalently to triangle 0 at the north pole. 18 | pub fn local_to_world( 19 | pos: Point3, 20 | dir: Dir, 21 | resolution: [GridCoord; 2], 22 | tri: &Triangle, 23 | ) -> (Point3, Dir) { 24 | // Compute rotation `dir` relative to world. 25 | let x_dir = tri.x_dir; 26 | let y_dir = (x_dir + 2) % 12; 27 | let x_edge_index = (x_dir / 2) as usize; 28 | let y_edge_index = (y_dir / 2) as usize; 29 | let transform_to_world = PosMat2::new( 30 | NEIGHBOR_OFFSETS[x_edge_index].0, 31 | NEIGHBOR_OFFSETS[y_edge_index].0, 32 | NEIGHBOR_OFFSETS[x_edge_index].1, 33 | NEIGHBOR_OFFSETS[y_edge_index].1, 34 | ); 35 | 36 | // Apply transform. 37 | let pos2 = Pos2::new(pos.x, pos.y); 38 | let mut new_pos2: Pos2 = transform_to_world * pos2; 39 | let new_dir = Dir::new((dir.index + x_dir) % 12); 40 | 41 | // Translate `pos` from being relative to `apex`, to being 42 | // relative to the world, ignoring orientation. 43 | // 44 | // Both parts of the apex are expressed in terms of x-dimension. 45 | let apex = Pos2::new(tri.apex[0], tri.apex[1]) * resolution[0]; 46 | new_pos2 += apex.coords; 47 | let mut new_pos = pos; 48 | new_pos.x = new_pos2.x; 49 | new_pos.y = new_pos2.y; 50 | 51 | (new_pos, new_dir) 52 | } 53 | 54 | /// Transform `pos` and `dir` to be relative to the given triangle's apex. 55 | pub fn world_to_local( 56 | pos: Point3, 57 | dir: Dir, 58 | resolution: [GridCoord; 2], 59 | tri: &Triangle, 60 | ) -> (Point3, Dir) { 61 | // Both parts of the apex are expressed in terms of x-dimension. 62 | let apex = Pos2::new(tri.apex[0], tri.apex[1]) * resolution[0]; 63 | 64 | // Translate `pos` relative to `apex` ignoring orientation. 65 | let pos2 = Pos2::new(pos.x, pos.y); 66 | let pos_from_tri_apex = Pos2::from(pos2 - apex); 67 | 68 | // Compute rotation required to express `pos` and `dir` relative to apex. 69 | let x_dir = tri.x_dir; 70 | let y_dir = (x_dir + 2) % 12; 71 | let x_edge_index = (x_dir / 2) as usize; 72 | let y_edge_index = (y_dir / 2) as usize; 73 | // Nalgebra's inverse is cautious (error checking) and is currently implemented 74 | // in a way that precludes inverting matrices of integers. 75 | // Fortunately this made me realise the determinant of our axis pairs is 76 | // always equal to 1, so we can save ourselves a bit of calculation here. 77 | let transform_to_local = PosMat2::new( 78 | NEIGHBOR_OFFSETS[y_edge_index].1, 79 | -NEIGHBOR_OFFSETS[y_edge_index].0, 80 | -NEIGHBOR_OFFSETS[x_edge_index].1, 81 | NEIGHBOR_OFFSETS[x_edge_index].0, 82 | ); 83 | 84 | // Apply transform. 85 | let new_pos2: Pos2 = transform_to_local * pos_from_tri_apex; 86 | let mut new_pos = pos; 87 | new_pos.x = new_pos2.x; 88 | new_pos.y = new_pos2.y; 89 | let new_dir = Dir::new((dir.index + 12 - x_dir) % 12); 90 | 91 | (new_pos, new_dir) 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | use crate::{Dir, Point3}; 98 | 99 | const RESOLUTION: [i64; 2] = [32, 64]; 100 | 101 | #[test] 102 | fn world_to_tri_0_facing_x_is_noop() { 103 | // Transform from north pole to north pole. 104 | let pos = Point3::default(); 105 | let dir = Dir::default(); 106 | let tri = &TRIANGLES[0]; 107 | let (new_pos, new_dir) = world_to_local(pos, dir, RESOLUTION, tri); 108 | // Should be no-op. 109 | assert_eq!(pos, new_pos); 110 | assert_eq!(dir, new_dir); 111 | } 112 | 113 | #[test] 114 | fn world_to_tri_0_facing_north_is_noop() { 115 | // Transform from north pole to north pole, 116 | // starting a bit south of the pole and pointing up. 117 | // NOTE: this isn't a valid direction to move in, 118 | // but that doesn't matter; it's still valid to transform. 119 | let pos = Point3::default().with_x(1).with_y(1); 120 | let dir = Dir::new(7); 121 | let tri = &TRIANGLES[0]; 122 | let (new_pos, new_dir) = world_to_local(pos, dir, RESOLUTION, tri); 123 | // Should be no-op. 124 | assert_eq!(pos, new_pos); 125 | assert_eq!(dir, new_dir); 126 | } 127 | 128 | #[test] 129 | fn world_to_tri_4() { 130 | // Transform from just below northern tropic, facing north-west. 131 | let pos = Point3::default().with_x(2).with_y(RESOLUTION[1] / 2 - 1); 132 | let dir = Dir::new(8); 133 | let tri = &TRIANGLES[4]; 134 | let (new_pos, new_dir) = world_to_local(pos, dir, RESOLUTION, tri); 135 | // Should now be just below north pole, facing west. 136 | assert_eq!(Point3::default().with_x(1).with_y(1), new_pos); 137 | assert_eq!(Dir::new(10), new_dir); 138 | } 139 | 140 | #[test] 141 | fn tri_4_to_world() { 142 | // Transform from just below north pole, facing west. 143 | 144 | let pos = Point3::default().with_x(1).with_y(1); 145 | let dir = Dir::new(10); 146 | let tri = &TRIANGLES[4]; 147 | let (new_pos, new_dir) = local_to_world(pos, dir, RESOLUTION, tri); 148 | 149 | // Should now be just below northern tropic, facing north-west. 150 | assert_eq!( 151 | Point3::default().with_x(2,).with_y(RESOLUTION[1] / 2 - 1,), 152 | new_pos 153 | ); 154 | assert_eq!(Dir::new(8), new_dir); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /planetkit-grid/src/movement/util.rs: -------------------------------------------------------------------------------- 1 | use crate::na; 2 | 3 | use crate::cell_shape::NEIGHBOR_OFFSETS; 4 | use crate::{Dir, GridCoord, Point3}; 5 | 6 | use super::transform::*; 7 | use super::triangles::*; 8 | 9 | /// Get next cell in direction faced by `dir`, without considering 10 | /// movement between roots. Note that this may therefore return positions 11 | /// outside the boundaries of `pos`'s current root. 12 | /// 13 | /// Returns an error if `dir` does not point to a direction that would 14 | /// represent an immediately adjacent cell if in a hexagon. (Movement 15 | /// toward vertices is undefined.) 16 | pub fn adjacent_pos_in_dir(pos: Point3, dir: Dir) -> Result { 17 | if !dir.points_at_hex_edge() { 18 | return Err(()); 19 | } 20 | 21 | // Find the (x, y) offset for `dir` and apply to `pos`. 22 | // Edge index is half the direction index, because direction 0 23 | // points at edge 0. 24 | let edge_index = (dir.index / 2) as usize; 25 | let (dx, dy) = NEIGHBOR_OFFSETS[edge_index]; 26 | Ok(Point3::new(pos.root, pos.x + dx, pos.y + dy, pos.z)) 27 | } 28 | 29 | // Transform (x, y, dir) back to local coordinates near where we started, 30 | // taking account any change in root required. 31 | pub fn transform_into_exit_triangle( 32 | pos: &mut Point3, 33 | dir: &mut Dir, 34 | resolution: [GridCoord; 2], 35 | exit: &Exit, 36 | ) { 37 | let exit_tri = &TRIANGLES[exit.triangle_index]; 38 | pos.root.index = (pos.root.index + exit.root_offset) % 5; 39 | let (new_pos, new_dir) = local_to_world(*pos, *dir, resolution, exit_tri); 40 | *pos = new_pos; 41 | *dir = new_dir; 42 | } 43 | 44 | /// Pick triangle with the closest apex that is oriented such that `pos` lies 45 | /// between its x-axis and y-axis. 46 | /// 47 | /// If `pos` is on a pentagon, you probably won't want this. 48 | /// Consider `triangle_on_pos_with_closest_mid_axis` instead? 49 | pub fn closest_triangle_to_point(pos: &Point3, resolution: [GridCoord; 2]) -> &'static Triangle { 50 | // First we filter down to those where 51 | // pos lies between the triangle's x-axis and y-axis. 52 | // (See diagram in `triangles.rs`.) 53 | // 54 | // It is important that we don't pick a differently-oriented 55 | // triangle with the same apex, because that would sometimes 56 | // lead us to unnecessarily transforming positions into 57 | // neighboring quads. (We try to maintain stability within a given 58 | // quad in general, and there's a bunch of logic around here in particular 59 | // that assumes that.) 60 | let candidate_triangles = if pos.x + pos.y < resolution[0] { 61 | &TRIANGLES[0..3] 62 | } else if pos.y < resolution[0] { 63 | &TRIANGLES[3..6] 64 | } else if pos.x + pos.y < resolution[1] { 65 | &TRIANGLES[6..9] 66 | } else { 67 | &TRIANGLES[9..12] 68 | }; 69 | 70 | // Pick the closest triangle. 71 | type Pos2 = na::Point2; 72 | let pos2 = Pos2::new(pos.x, pos.y); 73 | candidate_triangles 74 | .iter() 75 | .min_by_key(|triangle| { 76 | // Both parts of the apex are expressed in terms of x-dimension. 77 | let apex = Pos2::new(triangle.apex[0], triangle.apex[1]) * resolution[0]; 78 | let apex_to_pos = (pos2 - apex).abs(); 79 | // Hex distance from apex to pos 80 | apex_to_pos.x + apex_to_pos.y 81 | }) 82 | .expect("There should have been exactly three items; this shouldn't be possible!") 83 | } 84 | 85 | /// For whatever 1-3 triangles `pos` is sitting atop, find the one 86 | /// whose "middle axis" (half-way between x-axis and y-axis) is closest 87 | /// to `dir`. 88 | /// 89 | /// This is useful for re-basing while turning, without unnecessarily 90 | /// re-basing into a neighbouring root. 91 | /// 92 | /// Panics called with any pos that is not a pentagon. 93 | pub fn triangle_on_pos_with_closest_mid_axis( 94 | pos: &Point3, 95 | dir: Dir, 96 | resolution: [GridCoord; 2], 97 | ) -> &'static Triangle { 98 | // If `pos` sits on a pentagon and we're re-basing, then that probably 99 | // means we're turning. Because we're on a pentagon, it's important that 100 | // we select the triangle that is most closely oriented to our direction, 101 | // so that we don't accidentally re-base into a neighbouring quad unnecessarily. 102 | // (We try to maintain stability within a given quad in general, and there's a 103 | // bunch of logic around here in particular that assumes that.) 104 | type Pos2 = na::Point2; 105 | let pos2 = Pos2::new(pos.x, pos.y); 106 | TRIANGLES 107 | .iter() 108 | .filter(|triangle| { 109 | // There will be between one and three triangles that 110 | // we are exactly on top of. 111 | use num_traits::Zero; 112 | // Both parts of the apex are expressed in terms of x-dimension. 113 | let apex = Pos2::new(triangle.apex[0], triangle.apex[1]) * resolution[0]; 114 | let apex_to_pos = (pos2 - apex).abs(); 115 | apex_to_pos.is_zero() 116 | }) 117 | .min_by_key(|triangle| { 118 | // Find triangle with minimum angle between its "mid axis" 119 | // and wherever `pos` is pointing. 120 | let middle_axis_dir: i16 = (i16::from(triangle.x_dir) + 1) % 12; 121 | let mut a = middle_axis_dir - i16::from(dir.index); 122 | if a > 6 { 123 | a -= 12; 124 | } else if a < -6 { 125 | a += 12; 126 | } 127 | a.abs() 128 | }) 129 | .expect("There should have been 1-3 triangles; did you call this with a non-pentagon pos?") 130 | } 131 | 132 | pub fn is_pentagon(pos: &Point3, resolution: [GridCoord; 2]) -> bool { 133 | // There are six pentagons in every root quad: 134 | // 135 | // ◌ north 136 | // / \ 137 | // / \ 138 | // west ◌ ◌ north-east 139 | // \ \ 140 | // \ \ 141 | // south-west ◌ ◌ east 142 | // \ / 143 | // \ / 144 | // south ◌ 145 | // 146 | let is_north = pos.x == 0 && pos.y == 0; 147 | let is_north_east = pos.x == 0 && pos.y == resolution[0]; 148 | let is_east = pos.x == 0 && pos.y == resolution[1]; 149 | let is_west = pos.x == resolution[0] && pos.y == 0; 150 | let is_south_west = pos.x == resolution[0] && pos.y == resolution[0]; 151 | let is_south = pos.x == resolution[0] && pos.y == resolution[1]; 152 | is_north || is_north_east || is_east || is_west || is_south_west || is_south 153 | } 154 | 155 | pub fn is_on_root_edge(pos: &Point3, resolution: [GridCoord; 2]) -> bool { 156 | pos.x == 0 || pos.y == 0 || pos.x == resolution[0] || pos.y == resolution[1] 157 | } 158 | 159 | pub fn debug_assert_pos_within_root(pos: &mut Point3, resolution: [GridCoord; 2]) { 160 | debug_assert!( 161 | pos.x >= 0 && pos.y >= 0 && pos.x <= resolution[0] && pos.y <= resolution[1], 162 | "`pos` was outside its root at the given resolution." 163 | ); 164 | } 165 | -------------------------------------------------------------------------------- /planetkit-grid/src/neighbors.rs: -------------------------------------------------------------------------------- 1 | use super::{Dir, GridCoord, Point3, PosInOwningRoot}; 2 | use crate::movement::{move_forward, turn_left_by_one_hex_edge}; 3 | use std::iter::Chain; 4 | use std::slice; 5 | 6 | pub struct Neighbors { 7 | // Hide the iterators used to implement this. 8 | iter: NeighborsImpl, 9 | } 10 | 11 | /// Iterator over the cells sharing an interface with a given `pos`. 12 | /// Doesn't include diagonal neighbors, e.g., one across and one down. 13 | impl Neighbors { 14 | pub fn new(pos: Point3, resolution: [GridCoord; 2]) -> Neighbors { 15 | let above_and_below = AboveAndBelow::new(pos); 16 | let is_away_from_root_edges = 17 | pos.x > 0 && pos.x < resolution[0] - 1 && pos.y > 0 && pos.y < resolution[1] - 1; 18 | if is_away_from_root_edges { 19 | let fast_intra_root_neighbors = FastIntraRootNeighbors::new(pos); 20 | Neighbors { 21 | iter: NeighborsImpl::FastIntra(above_and_below.chain(fast_intra_root_neighbors)), 22 | } 23 | } else { 24 | let slow_general_edge_neighbors = SlowGeneralEdgeNeighbors::new(pos, resolution); 25 | Neighbors { 26 | iter: NeighborsImpl::SlowGeneral( 27 | above_and_below.chain(slow_general_edge_neighbors), 28 | ), 29 | } 30 | } 31 | } 32 | } 33 | 34 | impl Iterator for Neighbors { 35 | type Item = Point3; 36 | 37 | fn next(&mut self) -> Option { 38 | match self.iter { 39 | NeighborsImpl::SlowGeneral(ref mut iter) => iter.next(), 40 | NeighborsImpl::FastIntra(ref mut iter) => iter.next(), 41 | } 42 | } 43 | } 44 | 45 | // There are a couple of different implementations; we pick the fast 46 | // one if we can, or otherwise fall back to the general one. 47 | enum NeighborsImpl { 48 | SlowGeneral(Chain), 49 | FastIntra(Chain), 50 | } 51 | 52 | // TODO: this whole implementation is horribly inefficient, 53 | // but it was the easiest one I could think of to get up and 54 | // running quickly without having to handle a bunch of special 55 | // cases. Replace me! 56 | 57 | /// Iterator over the cells sharing an interface with a given `pos`. 58 | /// Doesn't include diagonal neighbors, e.g., one across and one down. 59 | struct SlowGeneralEdgeNeighbors { 60 | resolution: [GridCoord; 2], 61 | origin: Point3, 62 | first_neighbor: Option, 63 | current_dir: Dir, 64 | } 65 | 66 | impl SlowGeneralEdgeNeighbors { 67 | pub fn new(mut pos: Point3, resolution: [GridCoord; 2]) -> SlowGeneralEdgeNeighbors { 68 | // Pick a direction that's valid for `pos`. 69 | // To do this, first express the position in its owning root... 70 | pos = PosInOwningRoot::new(pos, resolution).into(); 71 | // ...after which we know that direction 0 is valid, except for 72 | // the south pole. Refer to the diagram in `PosInOwningRoot` 73 | // to see how this falls out. 74 | let start_dir = if pos.x == resolution[0] && pos.y == resolution[1] { 75 | Dir::new(6) 76 | } else { 77 | Dir::new(0) 78 | }; 79 | SlowGeneralEdgeNeighbors { 80 | resolution, 81 | origin: pos, 82 | first_neighbor: None, 83 | current_dir: start_dir, 84 | } 85 | } 86 | } 87 | 88 | impl Iterator for SlowGeneralEdgeNeighbors { 89 | type Item = Point3; 90 | 91 | fn next(&mut self) -> Option { 92 | // Find the neighbor in the current direction. 93 | let mut pos = self.origin; 94 | let mut dir = self.current_dir; 95 | move_forward(&mut pos, &mut dir, self.resolution) 96 | .expect("Oops, we started from an invalid position."); 97 | 98 | // Express neighbor in its owning root so we know 99 | // whether we've seen it twice. 100 | pos = PosInOwningRoot::new(pos, self.resolution).into(); 101 | 102 | if let Some(first_neighbor) = self.first_neighbor { 103 | if first_neighbor == pos { 104 | // We've already emitted this neighbor. 105 | return None; 106 | } 107 | } else { 108 | self.first_neighbor = Some(pos); 109 | } 110 | 111 | // Turn to face the next neighbor. 112 | turn_left_by_one_hex_edge(&mut self.origin, &mut self.current_dir, self.resolution) 113 | .expect("Oops, we picked a bad starting direction."); 114 | 115 | // Yield the neighbor we found above. 116 | Some(pos) 117 | } 118 | } 119 | 120 | /// Iterator over the cells sharing an interface with a given `pos`. 121 | /// Doesn't include diagonal neighbors, e.g., one across and one down. 122 | /// 123 | /// Assumes that all neighbors are within the same chunk as the center cell. 124 | /// Behaviour is undefined if this is not true. 125 | struct FastIntraRootNeighbors { 126 | origin: Point3, 127 | offsets: slice::Iter<'static, (GridCoord, GridCoord)>, 128 | } 129 | 130 | impl FastIntraRootNeighbors { 131 | pub fn new(pos: Point3) -> FastIntraRootNeighbors { 132 | use super::cell_shape::NEIGHBOR_OFFSETS; 133 | FastIntraRootNeighbors { 134 | origin: pos, 135 | offsets: NEIGHBOR_OFFSETS.iter(), 136 | } 137 | } 138 | } 139 | 140 | impl Iterator for FastIntraRootNeighbors { 141 | type Item = Point3; 142 | 143 | fn next(&mut self) -> Option { 144 | self.offsets.next().map(|offset| { 145 | self.origin 146 | .with_x(self.origin.x + offset.0) 147 | .with_y(self.origin.y + offset.1) 148 | }) 149 | } 150 | } 151 | 152 | // Iterator over cell positions immediately above and below 153 | // a given cell. 154 | // 155 | // Does not yield the (invalid) position below if the center cell is at `z == 0`. 156 | struct AboveAndBelow { 157 | origin: Point3, 158 | yielded_above: bool, 159 | yielded_below: bool, 160 | } 161 | 162 | impl AboveAndBelow { 163 | pub fn new(pos: Point3) -> AboveAndBelow { 164 | AboveAndBelow { 165 | origin: pos, 166 | yielded_above: false, 167 | yielded_below: false, 168 | } 169 | } 170 | } 171 | 172 | impl Iterator for AboveAndBelow { 173 | type Item = Point3; 174 | 175 | fn next(&mut self) -> Option { 176 | if !self.yielded_above { 177 | // Yield position above. 178 | self.yielded_above = true; 179 | return Some(self.origin.with_z(self.origin.z + 1)); 180 | } 181 | 182 | if !self.yielded_below { 183 | if self.origin.z == 0 { 184 | // There's no valid position below. 185 | return None; 186 | } 187 | 188 | // Yield position below. 189 | self.yielded_below = true; 190 | return Some(self.origin.with_z(self.origin.z - 1)); 191 | } 192 | 193 | None 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /planetkit-grid/src/root.rs: -------------------------------------------------------------------------------- 1 | pub type RootIndex = u8; 2 | 3 | #[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)] 4 | pub struct Root { 5 | pub index: RootIndex, 6 | } 7 | 8 | impl Root { 9 | pub fn new(index: RootIndex) -> Root { 10 | Root { index } 11 | } 12 | 13 | pub fn next_east(self) -> Root { 14 | Root { 15 | index: ((self.index + 1) % 5), 16 | } 17 | } 18 | 19 | pub fn next_west(self) -> Root { 20 | Root { 21 | index: ((self.index + (5 - 1)) % 5), 22 | } 23 | } 24 | } 25 | 26 | // TODO: we'll probably want to make this panic if you enter something 27 | // out of bounds, so this implementation is probably illegal. (IIRC `from` should not panic.) 28 | impl From for Root { 29 | fn from(root_index: RootIndex) -> Root { 30 | Root::new(root_index) 31 | } 32 | } 33 | 34 | // Occasionally useful around the place when we need to iterate over all roots. 35 | pub static ROOTS: [Root; 5] = [ 36 | Root { index: 0 }, 37 | Root { index: 1 }, 38 | Root { index: 2 }, 39 | Root { index: 3 }, 40 | Root { index: 4 }, 41 | ]; 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::Root; 46 | 47 | #[test] 48 | fn next_east() { 49 | let root: Root = 3.into(); 50 | assert_eq!(4, root.next_east().index); 51 | 52 | let root: Root = 4.into(); 53 | assert_eq!(0, root.next_east().index); 54 | } 55 | 56 | #[test] 57 | fn next_west() { 58 | let root: Root = 3.into(); 59 | assert_eq!(2, root.next_west().index); 60 | 61 | let root: Root = 0.into(); 62 | assert_eq!(4, root.next_west().index); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /planetkit.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "file_exclude_patterns": 6 | [ 7 | "*.sublime-workspace", 8 | "*.DS_Store", 9 | ".apdisk" 10 | ], 11 | "folder_exclude_patterns": 12 | [ 13 | "target" 14 | ], 15 | "path": "." 16 | }, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /planetkit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "planetkit" 3 | version = "0.0.1" 4 | authors = ["Jeff Parsons "] 5 | build = "build.rs" 6 | license = "MIT/Apache-2.0" 7 | description = """ 8 | High-level toolkit for building games based around voxel globes. 9 | """ 10 | repository = "https://github.com/jeffparsons/planetkit" 11 | edition = "2018" 12 | 13 | [features] 14 | nightly = [] 15 | web = [] 16 | default = ["desktop"] 17 | # All the stuff that won't work on the web. 18 | desktop = ["tokio-core", "tokio-io", "tokio-codec"] 19 | 20 | [lib] 21 | path = "src/lib.rs" 22 | 23 | [dependencies] 24 | planetkit-grid = { path = "../planetkit-grid" } 25 | bytes = "0.4.5" 26 | noise = "0.5.0" 27 | piston = "0.42" 28 | piston2d-graphics = "0.30" 29 | pistoncore-glutin_window = "0.54" 30 | piston2d-opengl_graphics = "0.59" 31 | gfx = "0.17" 32 | gfx_device_gl = "0.15" 33 | piston_window = "0.89" 34 | camera_controllers = "0.27" 35 | vecmath = "0.3.0" 36 | shader_version = "0.3.0" 37 | nalgebra = "0.18" 38 | ncollide3d = "0.19" 39 | nphysics3d = "0.11" 40 | rand = "0.6" 41 | rand_xoshiro = "0.1" 42 | slog = "2.0.4" 43 | slog-term = "2.0.0" 44 | slog-async = "2.0.1" 45 | chrono = "0.4.0" 46 | shred = "0.7" 47 | shred-derive = "0.5" 48 | specs = "0.14" 49 | num-traits = "0.2.0" 50 | itertools = "0.8" 51 | # At time of writing, only used for tests. 52 | approx = "0.3" 53 | froggy = "0.4.0" 54 | arrayvec = "0.4.5" 55 | futures = "0.1.14" 56 | serde = "1.0.10" 57 | serde_json = "1.0.2" 58 | serde_derive = "1.0.10" 59 | 60 | # Stuff we can't run on the web yet. 61 | [target.'cfg(not(target_os = "emscripten"))'.dependencies] 62 | tokio-core = { version = "0.1.17", optional = true } 63 | tokio-io = { version = "0.1.7", optional = true } 64 | tokio-codec = { version = "0.1.0", optional = true } 65 | 66 | [build-dependencies] 67 | rustc_version = "0.2.1" 68 | -------------------------------------------------------------------------------- /planetkit/build.rs: -------------------------------------------------------------------------------- 1 | extern crate rustc_version; 2 | 3 | use rustc_version::{version_meta, Channel}; 4 | 5 | fn main() { 6 | if version_meta().unwrap().channel == Channel::Nightly { 7 | println!("cargo:rustc-cfg=feature=\"nightly\""); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /planetkit/examples/quick_start.rs: -------------------------------------------------------------------------------- 1 | use planetkit as pk; 2 | 3 | fn main() { 4 | let mut app = pk::AppBuilder::new().with_common_systems().build_gui(); 5 | pk::simple::populate_world(app.world_mut()); 6 | app.run(); 7 | } 8 | -------------------------------------------------------------------------------- /planetkit/src/app_builder.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc; 2 | 3 | use shred; 4 | use slog; 5 | #[cfg(not(target_os = "emscripten"))] 6 | use slog_async; 7 | #[cfg(not(target_os = "emscripten"))] 8 | use slog_term; 9 | use specs; 10 | 11 | use crate::app::App; 12 | use crate::cell_dweller; 13 | use crate::net::{GameMessage, ServerResource}; 14 | use crate::window; 15 | 16 | /// Builder for [`App`]. 17 | /// 18 | /// Will eventually learn how to create different kinds of 19 | /// application runners, including a CLI-only one that doesn't 20 | /// need to include any renderering systems. 21 | /// 22 | /// Contains some optional convenience functions for adding 23 | /// commonly used systems. 24 | #[must_use] 25 | pub struct AppBuilder { 26 | root_log: slog::Logger, 27 | world: specs::World, 28 | dispatcher_builder: shred::DispatcherBuilder<'static, 'static>, 29 | // We may or may not create these, depending on the game. 30 | movement_input_adapter: Option>, 31 | mining_input_adapter: Option>, 32 | } 33 | 34 | impl AppBuilder { 35 | pub fn new() -> AppBuilder { 36 | use crate::LogResource; 37 | 38 | // Set up logger. 39 | // REVISIT: make logger configurable? E.g. based on whether on web or not. 40 | // Or just commit to a specific kind of drain for emscripten? 41 | #[cfg(not(target_os = "emscripten"))] 42 | let drain = { 43 | use slog::Drain; 44 | 45 | let decorator = slog_term::TermDecorator::new().build(); 46 | let drain = slog_term::FullFormat::new(decorator).build().fuse(); 47 | slog_async::Async::new(drain).build().fuse() 48 | }; 49 | #[cfg(target_os = "emscripten")] 50 | let drain = slog::Discard; 51 | let root_log = slog::Logger::root(drain, o!("pk_version" => env!("CARGO_PKG_VERSION"))); 52 | 53 | // Create world and register all component types. 54 | // TODO: move component type registration elsewhere; 55 | // AutoSystems that use them should ensure that they are registered. 56 | let mut world = specs::World::new(); 57 | world.register::(); 58 | world.register::(); 59 | world.register::(); 60 | world.register::(); 61 | world.register::(); 62 | world.register::(); 63 | world.register::(); 64 | world.register::(); 65 | 66 | // Initialize resources that can't implement `Default`. 67 | world.add_resource(LogResource::new(&root_log)); 68 | 69 | // NOTE: You must opt in to having a `ServerResource` 70 | // if you want it by calling `with_networking`. 71 | 72 | AppBuilder { 73 | root_log, 74 | world, 75 | dispatcher_builder: specs::DispatcherBuilder::new(), 76 | movement_input_adapter: None, 77 | mining_input_adapter: None, 78 | } 79 | } 80 | 81 | pub fn build_gui(self) -> App { 82 | // TODO: move that function into this file; it doesn't need its own module. 83 | let window = window::make_window(&self.root_log); 84 | 85 | // TODO: hand the root log over to App, rather than making it borrow it. 86 | let mut app = App::new(&self.root_log, window, self.world, self.dispatcher_builder); 87 | if let Some(movement_input_adapter) = self.movement_input_adapter { 88 | app.add_input_adapter(movement_input_adapter); 89 | } 90 | if let Some(mining_input_adapter) = self.mining_input_adapter { 91 | app.add_input_adapter(mining_input_adapter); 92 | } 93 | app 94 | } 95 | 96 | pub fn with_systems>(mut self, add_systems_fn: F) -> Self { 97 | self.dispatcher_builder = 98 | add_systems_fn(&self.root_log, &mut self.world, self.dispatcher_builder); 99 | self 100 | } 101 | 102 | // TODO: Remark (assert!) on how this must 103 | // be called before adding any networking-related systems. 104 | pub fn with_networking(mut self) -> Self { 105 | self.world 106 | .add_resource(ServerResource::::new(&self.root_log)); 107 | self 108 | } 109 | 110 | /// Add a few systems that you're likely to want, especially if you're just getting 111 | /// started with PlanetKit and want to get up and running quickly. 112 | pub fn with_common_systems(mut self) -> Self { 113 | use crate::globe; 114 | 115 | // Set up input adapters. 116 | let (movement_input_sender, movement_input_receiver) = mpsc::channel(); 117 | self.movement_input_adapter = Some(Box::new(cell_dweller::MovementInputAdapter::new( 118 | movement_input_sender, 119 | ))); 120 | 121 | let (mining_input_sender, mining_input_receiver) = mpsc::channel(); 122 | self.mining_input_adapter = Some(Box::new(cell_dweller::MiningInputAdapter::new( 123 | mining_input_sender, 124 | ))); 125 | 126 | let movement_sys = 127 | cell_dweller::MovementSystem::new(movement_input_receiver, &self.root_log); 128 | 129 | let mining_sys = cell_dweller::MiningSystem::new(mining_input_receiver, &self.root_log); 130 | 131 | let cd_physics_sys = cell_dweller::PhysicsSystem::new( 132 | &self.root_log, 133 | 0.1, // Seconds between falls 134 | ); 135 | 136 | let chunk_sys = globe::ChunkSystem::new(&self.root_log); 137 | 138 | let chunk_view_sys = globe::ChunkViewSystem::new( 139 | &self.root_log, 140 | 0.05, // Seconds between geometry creation 141 | ); 142 | 143 | self.with_systems( 144 | |_logger: &slog::Logger, 145 | _world: &mut specs::World, 146 | dispatcher_builder: specs::DispatcherBuilder<'static, 'static>| { 147 | dispatcher_builder 148 | // Try to get stuff most directly linked to input done first 149 | // to avoid another frame of lag. 150 | .with(movement_sys, "cd_movement", &[]) 151 | .with(mining_sys, "cd_mining", &["cd_movement"]) 152 | .with_barrier() 153 | .with(cd_physics_sys, "cd_physics", &[]) 154 | .with(chunk_sys, "chunk", &[]) 155 | // Don't depend on chunk system; chunk view can lag happily, so we'd prefer 156 | // to be able to run it in parallel. 157 | .with(chunk_view_sys, "chunk_view", &[]) 158 | }, 159 | ) 160 | } 161 | } 162 | 163 | pub trait AddSystemsFn<'a, 'b>: 164 | FnOnce( 165 | &slog::Logger, 166 | &mut specs::World, 167 | specs::DispatcherBuilder<'a, 'b>, 168 | ) -> specs::DispatcherBuilder<'a, 'b> 169 | { 170 | } 171 | 172 | impl<'a, 'b, F> AddSystemsFn<'a, 'b> for F where 173 | F: FnOnce( 174 | &slog::Logger, 175 | &mut specs::World, 176 | specs::DispatcherBuilder<'a, 'b>, 177 | ) -> specs::DispatcherBuilder<'a, 'b> 178 | { 179 | } 180 | 181 | impl Default for AppBuilder { 182 | fn default() -> Self { 183 | Self::new() 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /planetkit/src/camera/mod.rs: -------------------------------------------------------------------------------- 1 | use specs; 2 | 3 | /// Default camera to be used by render system. 4 | /// 5 | /// This is intended to be used as a Specs resource. 6 | #[derive(Default)] 7 | pub struct DefaultCamera { 8 | pub camera_entity: Option, 9 | } 10 | -------------------------------------------------------------------------------- /planetkit/src/cell_dweller/cell_dweller.rs: -------------------------------------------------------------------------------- 1 | use specs; 2 | 3 | use crate::globe::Spec; 4 | use crate::grid::{Dir, Point3}; 5 | use crate::movement::*; 6 | use crate::types::*; 7 | 8 | pub struct CellDweller { 9 | // TODO: make these private and use guts trait pattern to expose them internally. 10 | // TODO: is this guts pattern worth a separate macro crate of its own? 11 | pub pos: Point3, 12 | pub dir: Dir, 13 | pub last_turn_bias: TurnDir, 14 | // Most `CellDweller`s will also be `Spatial`s. Track whether the 15 | // computed real-space transform has been updated since the globe-space 16 | // transform was modified so we know when the former is dirty. 17 | is_real_space_transform_dirty: bool, 18 | pub globe_spec: Spec, 19 | pub seconds_between_moves: TimeDelta, 20 | pub seconds_until_next_move: TimeDelta, 21 | pub seconds_between_turns: TimeDelta, 22 | pub seconds_until_next_turn: TimeDelta, 23 | pub seconds_until_next_fall: TimeDelta, 24 | pub globe_entity: Option, 25 | } 26 | 27 | impl CellDweller { 28 | pub fn new( 29 | pos: Point3, 30 | dir: Dir, 31 | globe_spec: Spec, 32 | globe_entity: Option, 33 | ) -> CellDweller { 34 | CellDweller { 35 | pos, 36 | dir, 37 | last_turn_bias: TurnDir::Right, 38 | is_real_space_transform_dirty: true, 39 | globe_spec, 40 | // TODO: accept as parameter 41 | seconds_between_moves: 0.1, 42 | seconds_until_next_move: 0.0, 43 | // TODO: accept as parameter 44 | seconds_between_turns: 0.2, 45 | seconds_until_next_turn: 0.0, 46 | seconds_until_next_fall: 0.0, 47 | globe_entity, 48 | } 49 | } 50 | 51 | pub fn pos(&self) -> Point3 { 52 | self.pos 53 | } 54 | 55 | pub fn set_grid_point(&mut self, new_pos: Point3) { 56 | self.pos = new_pos; 57 | self.is_real_space_transform_dirty = true; 58 | } 59 | 60 | pub fn set_cell_transform( 61 | &mut self, 62 | new_pos: Point3, 63 | new_dir: Dir, 64 | new_last_turn_bias: TurnDir, 65 | ) { 66 | self.pos = new_pos; 67 | self.dir = new_dir; 68 | self.last_turn_bias = new_last_turn_bias; 69 | self.is_real_space_transform_dirty = true; 70 | } 71 | 72 | pub fn dir(&self) -> Dir { 73 | self.dir 74 | } 75 | 76 | pub fn turn(&mut self, turn_dir: TurnDir) { 77 | turn_by_one_hex_edge( 78 | &mut self.pos, 79 | &mut self.dir, 80 | self.globe_spec.root_resolution, 81 | turn_dir, 82 | ) 83 | .expect("This suggests a bug in `movement` code."); 84 | self.is_real_space_transform_dirty = true; 85 | } 86 | 87 | /// Calculate position in real-space. 88 | fn real_pos(&self) -> Pt3 { 89 | self.globe_spec.cell_bottom_center(self.pos) 90 | } 91 | 92 | fn real_transform(&self) -> Iso3 { 93 | let eye = self.real_pos(); 94 | // Look one cell ahead. 95 | let next_pos = adjacent_pos_in_dir(self.pos, self.dir).unwrap(); 96 | let target = self.globe_spec.cell_bottom_center(next_pos); 97 | // Calculate up vector. Nalgebra will normalise this so we can 98 | // just use the eye position as a vector; it points up out from 99 | // the center of the world already! 100 | let up = eye.coords; 101 | Iso3::face_towards(&eye, &target, &up) 102 | } 103 | 104 | // Make it a long cumbersome name so you make it explicit you're 105 | // not storing the result on a Spatial. 106 | pub fn real_transform_without_setting_clean(&self) -> Iso3 { 107 | self.real_transform() 108 | } 109 | 110 | pub fn is_real_space_transform_dirty(&self) -> bool { 111 | self.is_real_space_transform_dirty 112 | } 113 | 114 | // TODO: document responsibilities of caller. 115 | // TODO: return translation and orientation. 116 | pub fn get_real_transform_and_mark_as_clean(&mut self) -> Iso3 { 117 | self.is_real_space_transform_dirty = false; 118 | self.real_transform() 119 | } 120 | } 121 | 122 | impl specs::Component for CellDweller { 123 | type Storage = specs::HashMapStorage; 124 | } 125 | -------------------------------------------------------------------------------- /planetkit/src/cell_dweller/mining.rs: -------------------------------------------------------------------------------- 1 | use super::CellDweller; 2 | use crate::globe::chunk::{Cell, Material}; 3 | use crate::globe::Globe; 4 | use crate::grid::PosInOwningRoot; 5 | use crate::movement::*; 6 | 7 | /// Assumes that the given CellDweller is indeed attached to the given globe. 8 | /// May panick if this is not true. 9 | pub fn can_pick_up(cd: &mut CellDweller, globe: &mut Globe) -> bool { 10 | // Only allow picking stuff up if you're sitting above solid ground. 11 | // (Or, rather, the stuff we consider to be solid for now, 12 | // which is anything other than air.) 13 | // 14 | // TODO: abstract this whole thing... you need some kind of 15 | // utilities for a globe. 16 | if cd.pos.z < 0 { 17 | // There's nothing below; someone built a silly globe. 18 | return false; 19 | } 20 | let under_pos = cd.pos.with_z(cd.pos.z - 1); 21 | { 22 | // Inner scope to fight borrowck. 23 | let under_cell = match globe.maybe_non_authoritative_cell(under_pos) { 24 | Ok(cell) => cell, 25 | // Chunk not loaded; wait until it is before attempting to pick up. 26 | Err(_) => return false, 27 | }; 28 | if under_cell.material != Material::Dirt { 29 | return false; 30 | } 31 | } 32 | 33 | // Ask the globe if there's anything in front of us to "pick up". 34 | let mut new_pos = cd.pos; 35 | let mut new_dir = cd.dir; 36 | move_forward(&mut new_pos, &mut new_dir, globe.spec().root_resolution) 37 | .expect("CellDweller should have been in good state."); 38 | let anything_to_pick_up = { 39 | // Chunk might not be loaded; in that case assume nothing to pick up. 40 | globe 41 | .maybe_non_authoritative_cell(new_pos) 42 | .map(|cell| cell.material == Material::Dirt) 43 | .unwrap_or(false) 44 | }; 45 | // Also require that there's air above the block; 46 | // in my initial use case I don't want to allow mining below 47 | // the surface. 48 | let air_above_target = { 49 | // Chunk might not be loaded; in that case assume not air above block. 50 | let above_new_pos = new_pos.with_z(new_pos.z + 1); 51 | globe 52 | .maybe_non_authoritative_cell(above_new_pos) 53 | .map(|cell| cell.material == Material::Air) 54 | .unwrap_or(false) 55 | }; 56 | anything_to_pick_up && air_above_target 57 | } 58 | 59 | // If anything was picked up, then return the position we picked up, 60 | // and what was in it. 61 | pub fn pick_up_if_possible( 62 | cd: &mut CellDweller, 63 | globe: &mut Globe, 64 | ) -> Option<(PosInOwningRoot, Cell)> { 65 | if !can_pick_up(cd, globe) { 66 | return None; 67 | } 68 | 69 | let mut new_pos = cd.pos; 70 | let mut new_dir = cd.dir; 71 | move_forward(&mut new_pos, &mut new_dir, globe.spec().root_resolution) 72 | .expect("CellDweller should have been in good state."); 73 | 74 | // TODO: make a special kind of thing you can pick up. 75 | // TODO: accept that as a system argument, and have some builders 76 | // that make it super-easy to configure. 77 | // The goal here should be that the "block dude" game 78 | // ends up both concise and legible. 79 | let new_pos_in_owning_root = PosInOwningRoot::new(new_pos, globe.spec().root_resolution); 80 | 81 | let removed_cell = remove_block(globe, new_pos_in_owning_root); 82 | 83 | // We picked something up. 84 | Some((new_pos_in_owning_root, removed_cell)) 85 | } 86 | 87 | pub fn remove_block(globe: &mut Globe, pos_in_owning_root: PosInOwningRoot) -> Cell { 88 | use crate::globe::is_point_shared; 89 | 90 | // Keep for later, so we can return what was in it. 91 | let cloned_cell = { 92 | let cell = globe.authoritative_cell_mut(pos_in_owning_root); 93 | let cs = *cell; 94 | cell.material = Material::Air; 95 | cs 96 | }; 97 | 98 | // Some extra stuff is only relevant if the cell is shared 99 | // with another chunk (horizontal edges). 100 | if is_point_shared(*pos_in_owning_root.pos(), globe.spec().chunk_resolution) { 101 | // Bump version of owned shared cells. 102 | globe.increment_chunk_owned_edge_version_for_cell(pos_in_owning_root); 103 | // Propagate change to neighbouring chunks. 104 | let chunk_origin = globe.origin_of_chunk_owning(pos_in_owning_root); 105 | globe.push_shared_cells_for_chunk(chunk_origin); 106 | } 107 | // Mark the view for the containing chunk and those containing each cell surrounding 108 | // it as being dirty. (This cell might affect the visibility of cells in those chunks.) 109 | // TODO: different API where you commit to changing a cell 110 | // in a closure you get back that has a reference to it? 111 | // Or contains a _wrapper_ around it so it knows if you mutated it? Ooooh. 112 | globe.mark_chunk_views_affected_by_cell_as_dirty(pos_in_owning_root.into()); 113 | // TODO: remember on the cell-dweller that it's carrying something? 114 | // Or should that be a different kind of component? 115 | 116 | cloned_cell 117 | } 118 | -------------------------------------------------------------------------------- /planetkit/src/cell_dweller/mining_system.rs: -------------------------------------------------------------------------------- 1 | use piston::input::Input; 2 | use slog::Logger; 3 | use specs; 4 | use specs::{Read, ReadStorage, Write, WriteStorage}; 5 | use std::sync::mpsc; 6 | 7 | use super::{ 8 | ActiveCellDweller, CellDweller, CellDwellerMessage, SendMessageQueue, TryPickUpBlockMessage, 9 | }; 10 | use crate::globe::Globe; 11 | use crate::input_adapter; 12 | use crate::net::{Destination, NetMarker, SendMessage, Transport}; 13 | 14 | // TODO: own file? 15 | pub struct MiningInputAdapter { 16 | sender: mpsc::Sender, 17 | } 18 | 19 | impl MiningInputAdapter { 20 | pub fn new(sender: mpsc::Sender) -> MiningInputAdapter { 21 | MiningInputAdapter { sender } 22 | } 23 | } 24 | 25 | impl input_adapter::InputAdapter for MiningInputAdapter { 26 | fn handle(&self, input_event: &Input) { 27 | use piston::input::keyboard::Key; 28 | use piston::input::{Button, ButtonState}; 29 | 30 | if let Input::Button(button_args) = *input_event { 31 | if let Button::Keyboard(key) = button_args.button { 32 | let is_down = match button_args.state { 33 | ButtonState::Press => true, 34 | ButtonState::Release => false, 35 | }; 36 | match key { 37 | Key::U => self.sender.send(MiningEvent::PickUp(is_down)).unwrap(), 38 | _ => (), 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | pub enum MiningEvent { 46 | PickUp(bool), 47 | } 48 | 49 | pub struct MiningSystem { 50 | input_receiver: mpsc::Receiver, 51 | log: Logger, 52 | // TODO: need a better way to deal with one-off events: 53 | // just set pick_up to false once we've processed it? 54 | // But Piston seems to have some kind of silly key-repeat thing built in. 55 | // TODO: clarify. 56 | pick_up: bool, 57 | } 58 | 59 | impl MiningSystem { 60 | pub fn new(input_receiver: mpsc::Receiver, parent_log: &Logger) -> MiningSystem { 61 | MiningSystem { 62 | input_receiver, 63 | log: parent_log.new(o!()), 64 | pick_up: false, 65 | } 66 | } 67 | 68 | fn consume_input(&mut self) { 69 | loop { 70 | match self.input_receiver.try_recv() { 71 | Ok(MiningEvent::PickUp(b)) => self.pick_up = b, 72 | Err(_) => return, 73 | } 74 | } 75 | } 76 | } 77 | 78 | impl<'a> specs::System<'a> for MiningSystem { 79 | type SystemData = ( 80 | WriteStorage<'a, CellDweller>, 81 | WriteStorage<'a, Globe>, 82 | Read<'a, ActiveCellDweller>, 83 | Write<'a, SendMessageQueue>, 84 | ReadStorage<'a, NetMarker>, 85 | ); 86 | 87 | fn run(&mut self, data: Self::SystemData) { 88 | self.consume_input(); 89 | 90 | let ( 91 | mut cell_dwellers, 92 | mut globes, 93 | active_cell_dweller_resource, 94 | mut send_message_queue, 95 | net_markers, 96 | ) = data; 97 | let active_cell_dweller_entity = match active_cell_dweller_resource.maybe_entity { 98 | Some(entity) => entity, 99 | None => return, 100 | }; 101 | let cd = cell_dwellers 102 | .get_mut(active_cell_dweller_entity) 103 | .expect("Someone deleted the controlled entity's CellDweller"); 104 | 105 | // Get the associated globe, complaining loudly if we fail. 106 | let globe_entity = match cd.globe_entity { 107 | Some(globe_entity) => globe_entity, 108 | None => { 109 | warn!( 110 | self.log, 111 | "There was no associated globe entity or it wasn't actually a Globe! Can't proceed!" 112 | ); 113 | return; 114 | } 115 | }; 116 | let globe = match globes.get_mut(globe_entity) { 117 | Some(globe) => globe, 118 | None => { 119 | warn!( 120 | self.log, 121 | "The globe associated with this CellDweller is not alive! Can't proceed!" 122 | ); 123 | return; 124 | } 125 | }; 126 | 127 | // If we're trying to pick up, and from our perspective (we might not be the server) 128 | // we _can_ pick up, then request to the server to pick up the block. 129 | if self.pick_up && super::mining::can_pick_up(cd, globe) { 130 | // Post a message to the server (even if that's us) 131 | // requesting to remove the block. 132 | debug!(self.log, "Requesting to pick up a block"); 133 | 134 | if send_message_queue.has_consumer { 135 | // If there's a network consumer, then presumably 136 | // the entity has been given a global ID. 137 | let cd_entity_id = net_markers 138 | .get(active_cell_dweller_entity) 139 | .expect("Shouldn't be trying to tell peers about entities that don't have global IDs!") 140 | .id; 141 | send_message_queue.queue.push_back(SendMessage { 142 | // Send the request to the master node, including when that's us. 143 | destination: Destination::Master, 144 | game_message: CellDwellerMessage::TryPickUpBlock(TryPickUpBlockMessage { 145 | cd_entity_id, 146 | }), 147 | transport: Transport::TCP, 148 | }) 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /planetkit/src/cell_dweller/mod.rs: -------------------------------------------------------------------------------- 1 | // It's a private module; allow this. 2 | // (It's just used for grouping implementation code; 3 | // not in any public interface. Maybe one day I'll revisit 4 | // this and make the internal organisation a bit better, 5 | // but I don't want to be bugged about it for now.) 6 | #[allow(clippy::module_inception)] 7 | mod cell_dweller; 8 | mod mining; 9 | mod mining_system; 10 | mod movement_system; 11 | mod physics_system; 12 | mod recv_system; 13 | 14 | use crate::grid::{Dir, Point3}; 15 | use crate::movement::TurnDir; 16 | use crate::net::{RecvMessage, SendMessage}; 17 | use std::collections::vec_deque::VecDeque; 18 | 19 | pub use self::cell_dweller::CellDweller; 20 | pub use self::mining_system::{MiningEvent, MiningInputAdapter, MiningSystem}; 21 | pub use self::movement_system::{MovementEvent, MovementInputAdapter, MovementSystem}; 22 | pub use self::physics_system::PhysicsSystem; 23 | pub use self::recv_system::RecvSystem; 24 | 25 | use specs; 26 | 27 | /// `World`-global resource for finding the current cell-dwelling entity being controlled 28 | /// by the player, if any. 29 | /// 30 | /// TODO: make this a more general "controlled entity" somewhere? 31 | #[derive(Default)] 32 | pub struct ActiveCellDweller { 33 | pub maybe_entity: Option, 34 | } 35 | 36 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 37 | pub enum CellDwellerMessage { 38 | SetPos(SetPosMessage), 39 | TryPickUpBlock(TryPickUpBlockMessage), 40 | RemoveBlock(RemoveBlockMessage), 41 | } 42 | 43 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 44 | pub struct SetPosMessage { 45 | pub entity_id: u64, 46 | pub new_pos: Point3, 47 | pub new_dir: Dir, 48 | pub new_last_turn_bias: TurnDir, 49 | } 50 | 51 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 52 | pub struct TryPickUpBlockMessage { 53 | // TODO: 54 | // pub globe_entity_id: u64, 55 | pub cd_entity_id: u64, 56 | // TODO: what are you trying to pick up? Until we hook that up, 57 | // just use whatever the server thinks is in front of you. 58 | // pub pos: Point3, 59 | // TODO: also include the cell dweller's current position. 60 | // We'll trust that if it's close enough, so that we don't 61 | // have to worry about missing out on a position update and 62 | // picking up a different block than what the client believed 63 | // they were pickng up! 64 | } 65 | 66 | // TODO: this shouldn't really even be a cell dweller message; 67 | // it's a more general thing. But it's also not how we want to 68 | // represent the concept of chunk changes long-term, anyway, 69 | // so just leave it in here for now. Hoo boy, lotsa refactoring 70 | // lies ahead. 71 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] 72 | pub struct RemoveBlockMessage { 73 | // TODO: identify the globe. 74 | // But for that, the server will need to communicate the globe's 75 | // identity etc. to the client when they join. 76 | // For now it's just going to find the first globe it can... :) 77 | // pub globe_entity_id: u64, 78 | 79 | // Don't send it as a "PosInOwningRoot", because we can't trust 80 | // clients like that. 81 | // 82 | // TODO: We should actually be validating EVERYTHING that comes 83 | // in as a network message. 84 | pub pos: Point3, 85 | } 86 | 87 | /// `World`-global resource for outbound cell-dweller network messages. 88 | #[derive(Default)] 89 | pub struct SendMessageQueue { 90 | // We don't want to queue up any messages unless there's 91 | // actually a network system hanging around to consume them. 92 | // TODO: there's got to be a better way to do this. 93 | // I'm thinking some kind of simple pubsub, that doesn't 94 | // know anything about atomics/thread synchronisation, 95 | // but is instead just a dumb collection of `VecDeque`s. 96 | // As you add more of these, either find something that works 97 | // or make that thing you described above. 98 | pub has_consumer: bool, 99 | pub queue: VecDeque>, 100 | } 101 | 102 | /// `World`-global resource for inbound cell-dweller network messages. 103 | #[derive(Default)] 104 | pub struct RecvMessageQueue { 105 | pub queue: VecDeque>, 106 | } 107 | -------------------------------------------------------------------------------- /planetkit/src/cell_dweller/physics_system.rs: -------------------------------------------------------------------------------- 1 | use slog::Logger; 2 | use specs; 3 | use specs::{Read, ReadStorage, WriteStorage}; 4 | 5 | use super::CellDweller; 6 | use crate::globe::chunk::Material; 7 | use crate::globe::Globe; 8 | use crate::types::*; 9 | use crate::Spatial; 10 | 11 | pub struct PhysicsSystem { 12 | log: Logger, 13 | pub seconds_between_falls: TimeDelta, 14 | } 15 | 16 | impl PhysicsSystem { 17 | pub fn new(parent_log: &Logger, seconds_between_falls: TimeDelta) -> PhysicsSystem { 18 | PhysicsSystem { 19 | log: parent_log.new(o!()), 20 | seconds_between_falls, 21 | } 22 | } 23 | 24 | // Fall under the force of gravity if there's anywhere to fall to. 25 | // Note that "gravity" moves you down at a constant speed; 26 | // i.e. it doesn't accelerate you like in the real world. 27 | fn maybe_fall(&self, cd: &mut CellDweller, globe: &Globe, dt: TimeDelta) { 28 | // Only make you fall if there's air below you. 29 | if cd.pos.z <= 0 { 30 | // There's nothing below; someone built a silly globe. 31 | return; 32 | } 33 | let under_pos = cd.pos.with_z(cd.pos.z - 1); 34 | let under_cell = match globe.maybe_non_authoritative_cell(under_pos) { 35 | Ok(cell) => cell, 36 | // Chunk not loaded; wait until it is before attempting to fall. 37 | Err(_) => return, 38 | }; 39 | 40 | if under_cell.material == Material::Dirt { 41 | // Reset time until we can fall to the time 42 | // between falls; we don't want to instantly 43 | // fall down every step of size 1. 44 | cd.seconds_until_next_fall = self.seconds_between_falls; 45 | return; 46 | } 47 | 48 | // Count down until we're allowed to fall next. 49 | if cd.seconds_until_next_fall > 0.0 { 50 | cd.seconds_until_next_fall = (cd.seconds_until_next_fall - dt).max(0.0); 51 | } 52 | let still_waiting_to_fall = cd.seconds_until_next_fall > 0.0; 53 | if still_waiting_to_fall { 54 | return; 55 | } 56 | 57 | // Move down by one cell. 58 | cd.set_grid_point(under_pos); 59 | // REVISIT: += ? 60 | cd.seconds_until_next_fall = self.seconds_between_falls; 61 | trace!(self.log, "Fell under force of gravity"; "new_pos" => format!("{:?}", cd.pos())); 62 | } 63 | } 64 | 65 | impl<'a> specs::System<'a> for PhysicsSystem { 66 | type SystemData = ( 67 | Read<'a, TimeDeltaResource>, 68 | WriteStorage<'a, CellDweller>, 69 | WriteStorage<'a, Spatial>, 70 | ReadStorage<'a, Globe>, 71 | ); 72 | 73 | fn run(&mut self, data: Self::SystemData) { 74 | use specs::Join; 75 | let (dt, mut cell_dwellers, mut spatials, globes) = data; 76 | for (cd, spatial) in (&mut cell_dwellers, &mut spatials).join() { 77 | // Get the associated globe, complaining loudly if we fail. 78 | let globe_entity = match cd.globe_entity { 79 | Some(globe_entity) => globe_entity, 80 | None => { 81 | warn!( 82 | self.log, 83 | "There was no associated globe entity or it wasn't actually a Globe! Can't proceed!" 84 | ); 85 | continue; 86 | } 87 | }; 88 | let globe = match globes.get(globe_entity) { 89 | Some(globe) => globe, 90 | None => { 91 | warn!( 92 | self.log, 93 | "The globe associated with this CellDweller is not alive! Can't proceed!" 94 | ); 95 | continue; 96 | } 97 | }; 98 | 99 | self.maybe_fall(cd, globe, dt.0); 100 | 101 | // Update real-space coordinates if necessary. 102 | // TODO: do this in a separate system; it needs to be done before 103 | // things are rendered, but there might be other effects like gravity, 104 | // enemies shunting the cell dweller around, etc. that happen 105 | // after control. 106 | if cd.is_real_space_transform_dirty() { 107 | spatial.set_local_transform(cd.get_real_transform_and_mark_as_clean()); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /planetkit/src/globe/chunk_origin.rs: -------------------------------------------------------------------------------- 1 | use crate::grid::{GridCoord, Point3}; 2 | 3 | /// Wrapper type around a `Pos` that is known to express 4 | /// a valid chunk origin. 5 | /// 6 | /// Note that this does not save you from accidentally using 7 | /// positions from multiple incompatible `Globe`s with different 8 | /// resolutions. 9 | #[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash)] 10 | pub struct ChunkOrigin { 11 | pos: Point3, 12 | } 13 | 14 | impl Into for ChunkOrigin { 15 | fn into(self) -> Point3 { 16 | self.pos 17 | } 18 | } 19 | 20 | impl ChunkOrigin { 21 | // Asserts that `pos` is a valid chunk origin at the given `resolution`, 22 | // and returns a `ChunkOrigin` wrapping it. 23 | // 24 | // # Panics 25 | // 26 | // Panics if `pos` is not a valid chunk origin. 27 | // 28 | // TODO: replace these with debug_asserts and drop the promise of a panic above? 29 | pub fn new( 30 | pos: Point3, 31 | root_resolution: [GridCoord; 2], 32 | chunk_resolution: [GridCoord; 3], 33 | ) -> ChunkOrigin { 34 | // Make sure `pos` is within bounds. 35 | assert!(pos.x >= 0); 36 | assert!(pos.y >= 0); 37 | assert!(pos.z >= 0); 38 | assert!(pos.x < root_resolution[0]); 39 | assert!(pos.y < root_resolution[1]); 40 | 41 | // Chunk origins sit at multiples of `chunk_resolution[axis_index]`. 42 | assert_eq!(pos.x, pos.x / chunk_resolution[0] * chunk_resolution[0]); 43 | assert_eq!(pos.y, pos.y / chunk_resolution[1] * chunk_resolution[1]); 44 | assert_eq!(pos.z, pos.z / chunk_resolution[2] * chunk_resolution[2]); 45 | 46 | ChunkOrigin { pos } 47 | } 48 | } 49 | 50 | // TODO: Should this actually be an implementation of Deref? Try it... 51 | impl<'a> ChunkOrigin { 52 | pub fn pos(&'a self) -> &'a Point3 { 53 | &self.pos 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /planetkit/src/globe/chunk_pair.rs: -------------------------------------------------------------------------------- 1 | //! Tracking cells shared between neighboring chunks. 2 | 3 | use super::ChunkOrigin; 4 | use crate::grid::{Point3, PosInOwningRoot}; 5 | 6 | #[derive(Hash, PartialEq, Eq)] 7 | pub struct ChunkPairOrigins { 8 | pub source: ChunkOrigin, 9 | pub sink: ChunkOrigin, 10 | } 11 | 12 | pub struct ChunkPair { 13 | pub point_pairs: Vec, 14 | pub last_upstream_edge_version_known_downstream: u64, 15 | } 16 | 17 | #[derive(Clone)] 18 | pub struct PointPair { 19 | pub source: PosInOwningRoot, 20 | pub sink: Point3, 21 | } 22 | -------------------------------------------------------------------------------- /planetkit/src/globe/chunk_shared_points.rs: -------------------------------------------------------------------------------- 1 | use itertools; 2 | use std::ops; 3 | 4 | use super::ChunkOrigin; 5 | use crate::grid::{GridCoord, Point3, Root}; 6 | 7 | /// Iterate over all the points in a chunk that are shared with any 8 | /// other chunk. That is, those on the planes of x=0, x=max, y=0, and y=max, 9 | /// but neither the top nor bottom planes. 10 | pub struct ChunkSharedPoints { 11 | // TODO: optimise this to never even consider the internal grid points; 12 | // this is quite easy to write out as a product of chains or iterators, 13 | // but they have maps (with closures) in the middle, so I'm not sure 14 | // how to write their type out so I can store it. 15 | root: Root, 16 | x_min: GridCoord, 17 | x_max: GridCoord, 18 | y_min: GridCoord, 19 | y_max: GridCoord, 20 | iter: itertools::ConsTuples< 21 | itertools::Product< 22 | itertools::Product, ops::RangeInclusive>, 23 | ops::Range, 24 | >, 25 | ((GridCoord, GridCoord), GridCoord), 26 | >, 27 | } 28 | 29 | impl ChunkSharedPoints { 30 | pub fn new(chunk_origin: ChunkOrigin, chunk_resolution: [GridCoord; 3]) -> ChunkSharedPoints { 31 | let pos = chunk_origin.pos(); 32 | let iter = iproduct!( 33 | // Include the far edge. 34 | pos.x..=(pos.x + chunk_resolution[0]), 35 | pos.y..=(pos.y + chunk_resolution[1]), 36 | // Chunks don't share points in the z-direction, 37 | // but do in the x- and y-directions. 38 | pos.z..(pos.z + chunk_resolution[2]) 39 | ); 40 | ChunkSharedPoints { 41 | root: pos.root, 42 | x_min: pos.x, 43 | x_max: pos.x + chunk_resolution[0], 44 | y_min: pos.y, 45 | y_max: pos.y + chunk_resolution[1], 46 | iter, 47 | } 48 | } 49 | } 50 | 51 | impl Iterator for ChunkSharedPoints { 52 | type Item = Point3; 53 | 54 | fn next(&mut self) -> Option { 55 | if let Some(xyz) = self.iter.next() { 56 | let (x, y, z) = xyz; 57 | // Only return points that are on x=0, y=0, x=max, or y=max. 58 | let is_x_lim = x == self.x_min || x == self.x_max; 59 | let is_y_lim = y == self.y_min || y == self.y_max; 60 | // TODO: use `is_point_shared` instead. 61 | // 62 | // TODO: Rewrite this whole file to use a pre-computed 63 | // list for a given chunk size. (Or just cached on the Globe). 64 | // Because this whole thing is just terribly slow. 65 | let is_shared_point = is_x_lim || is_y_lim; 66 | if is_shared_point { 67 | // It's an x-edge or y-edge point. 68 | Some(Point3::new(self.root, x, y, z)) 69 | } else { 70 | // Skip it. 71 | self.next() 72 | } 73 | } else { 74 | None 75 | } 76 | } 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::*; 82 | use std::collections::HashSet; 83 | 84 | #[test] 85 | fn chunk_shared_points() { 86 | const ROOT_RESOLUTION: [GridCoord; 2] = [16, 32]; 87 | const CHUNK_RESOLUTION: [GridCoord; 3] = [8, 8, 64]; 88 | let chunk_origin = ChunkOrigin::new( 89 | Point3::new( 90 | // Arbitrary; just to make sure it flows throught to the chunk origin returned 91 | 4.into(), 92 | 8, 93 | 8, 94 | 192, 95 | ), 96 | ROOT_RESOLUTION, 97 | CHUNK_RESOLUTION, 98 | ); 99 | let shared_points_iter = ChunkSharedPoints::new(chunk_origin, CHUNK_RESOLUTION); 100 | let shared_points: Vec = shared_points_iter.collect(); 101 | // Should have as many points as the whole chunk minus the column down 102 | // the middle of non-shared cells. 103 | assert_eq!(shared_points.len(), 9 * 9 * 64 - 7 * 7 * 64); 104 | // TODO: better assertions? 105 | } 106 | 107 | #[test] 108 | fn all_shared_points_are_in_same_chunk() { 109 | use crate::globe::origin_of_chunk_in_same_root_containing; 110 | 111 | const ROOT_RESOLUTION: [GridCoord; 2] = [16, 32]; 112 | const CHUNK_RESOLUTION: [GridCoord; 3] = [8, 8, 64]; 113 | let chunk_origin = ChunkOrigin::new( 114 | Point3::new( 115 | // Arbitrary; just to make sure it flows throught to the chunk origin returned 116 | 4.into(), 117 | 8, 118 | 8, 119 | 192, 120 | ), 121 | ROOT_RESOLUTION, 122 | CHUNK_RESOLUTION, 123 | ); 124 | let origins_of_shared_points_iter = ChunkSharedPoints::new(chunk_origin, CHUNK_RESOLUTION) 125 | .map(|point| { 126 | origin_of_chunk_in_same_root_containing(point, ROOT_RESOLUTION, CHUNK_RESOLUTION) 127 | }); 128 | let origins: HashSet = origins_of_shared_points_iter.collect(); 129 | for origin in &origins { 130 | assert_eq!(origin.pos().root.index, 4); 131 | assert!(origin.pos().x >= chunk_origin.pos().x); 132 | assert!(origin.pos().x <= chunk_origin.pos().x + CHUNK_RESOLUTION[0]); 133 | assert!(origin.pos().y >= chunk_origin.pos().y); 134 | assert!(origin.pos().y <= chunk_origin.pos().y + CHUNK_RESOLUTION[1]); 135 | assert!(origin.pos().z >= chunk_origin.pos().z); 136 | assert!(origin.pos().z < chunk_origin.pos().z + CHUNK_RESOLUTION[2]); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /planetkit/src/globe/chunk_view.rs: -------------------------------------------------------------------------------- 1 | use specs; 2 | 3 | use crate::globe::ChunkOrigin; 4 | 5 | pub struct ChunkView { 6 | pub globe_entity: specs::Entity, 7 | pub origin: ChunkOrigin, 8 | } 9 | 10 | impl ChunkView { 11 | pub fn new(globe_entity: specs::Entity, origin: ChunkOrigin) -> ChunkView { 12 | ChunkView { 13 | origin, 14 | globe_entity, 15 | } 16 | } 17 | } 18 | 19 | impl specs::Component for ChunkView { 20 | type Storage = specs::HashMapStorage; 21 | } 22 | -------------------------------------------------------------------------------- /planetkit/src/globe/gen.rs: -------------------------------------------------------------------------------- 1 | use noise; 2 | 3 | use super::chunk::{Cell, Material}; 4 | use super::spec::Spec; 5 | use crate::globe::ChunkOrigin; 6 | use crate::grid::{Point2, Point3}; 7 | 8 | // TODO: turn this into a component that we can slap onto a Globe 9 | // or other globe-oid (distant point?). 10 | 11 | /// Globe content generator. Stores all the state for generating 12 | /// the terrain and any other parts of the globe that are derived 13 | /// from its seed. 14 | /// 15 | /// Will eventually do some basic caching, etc., but is pretty dumb 16 | /// right now. 17 | /// 18 | /// The plan is for this to eventually be used with multiple 19 | /// implementations of globes, e.g., a full voxmap based globe, 20 | /// a distant blob in the sky, to a shiny dot in the distance. 21 | pub trait Gen: Send + Sync { 22 | fn land_height(&self, column: Point2) -> f64; 23 | fn cell_at(&self, grid_point: Point3) -> Cell; 24 | fn populate_cells(&self, origin: ChunkOrigin, cells: &mut Vec); 25 | } 26 | 27 | pub struct SimpleGen { 28 | spec: Spec, 29 | terrain_noise: noise::Fbm, 30 | } 31 | 32 | impl SimpleGen { 33 | pub fn new(spec: Spec) -> SimpleGen { 34 | use noise::MultiFractal; 35 | use noise::Seedable; 36 | 37 | assert!(spec.is_valid(), "Invalid globe spec!"); 38 | 39 | // TODO: get parameters from spec 40 | // 41 | // TODO: store this function... when you figure 42 | // out what's going on with the types. 43 | // ("expected fn pointer, found fn item") 44 | // 45 | // TODO: even more pressing now that the noise API has 46 | // changed to deprecate PermutationTable; is it now stored 47 | // within Fbm? This might be super-slow now... 48 | let terrain_noise = noise::Fbm::new() 49 | // TODO: make wavelength etc. part of spec; 50 | // the octaves and wavelength of noise you want 51 | // will probably depend on planet size. 52 | .set_octaves(6) 53 | .set_frequency(1.0 / 700.0) 54 | // Truncate seed to make it fit what `noise` expects. 55 | .set_seed(spec.seed as u32); 56 | SimpleGen { 57 | spec, 58 | terrain_noise, 59 | } 60 | } 61 | } 62 | 63 | impl Gen for SimpleGen { 64 | fn land_height(&self, column: Point2) -> f64 { 65 | use noise::NoiseFn; 66 | 67 | // Calculate height for this cell from world spec. 68 | // To do this, project the cell onto a sea-level sphere 69 | // and sample 3D simplex noise to get a height value. 70 | // 71 | // Basing on sea-level lets us use similar wavelengths 72 | // to similar effect, regardless of the globe radius. 73 | // 74 | // TODO: split out a proper world generator 75 | // that layers in lots of different kinds of noise etc. 76 | let sea_level_pt3 = self.spec.cell_center_on_unit_sphere(column) * self.spec.ocean_radius; 77 | // Vary a little bit around 1.0. 78 | let delta = self.terrain_noise.get([sea_level_pt3.x, sea_level_pt3.y, sea_level_pt3.z]) 79 | * (self.spec.ocean_radius - self.spec.floor_radius) 80 | // TODO: this 0.9 is only to stop the dirt level 81 | // going below bedrock. Need something a bit more sophisticated 82 | // than this eventually. 83 | // 84 | // Also... OpenSimplex, which FBM uses, appears to be totally bonkers? 85 | // https://github.com/brendanzab/noise-rs/issues/149 86 | * 0.45; 87 | self.spec.ocean_radius + delta 88 | } 89 | 90 | fn cell_at(&self, grid_point: Point3) -> Cell { 91 | let land_height = self.land_height(grid_point.rxy); 92 | let cell_pt3 = self.spec.cell_center_center(grid_point); 93 | // TEMP: ... 94 | let cell_height = cell_pt3.coords.norm(); 95 | let material = if cell_height < land_height { 96 | Material::Dirt 97 | } else if cell_height < self.spec.ocean_radius { 98 | Material::Water 99 | } else { 100 | Material::Air 101 | }; 102 | Cell { 103 | material, 104 | // `Globe` fills this in; it's not really a property 105 | // of the naturally generated world, and it's not 106 | // deterministic from the world seed, so we don't 107 | // want to pollute `Gen` with it. 108 | // 109 | // TODO: probably remove this? We're just using 110 | // temporarily to create some texture across 111 | // cells to make them easy to tell apart and look 112 | // kinda nice, but this probably isn't a great 113 | // long-term solution... 114 | shade: 1.0, 115 | } 116 | } 117 | 118 | fn populate_cells(&self, origin: ChunkOrigin, cells: &mut Vec) { 119 | use rand; 120 | use rand::Rng; 121 | 122 | // We should be passed an empty vector to populate. 123 | assert!(cells.is_empty()); 124 | 125 | let chunk_res = &self.spec.chunk_resolution; 126 | let origin = origin.pos(); 127 | 128 | // Include cells _on_ the far edge of the chunk; 129 | // even though we don't own them we'll need to draw part of them. 130 | let end_x = origin.x + chunk_res[0]; 131 | let end_y = origin.y + chunk_res[1]; 132 | // Chunks don't share cells in the z-direction, 133 | // but do in the x- and y-directions. 134 | let end_z = origin.z + chunk_res[2] - 1; 135 | for cell_z in origin.z..=end_z { 136 | for cell_y in origin.y..=end_y { 137 | for cell_x in origin.x..=end_x { 138 | let grid_point = Point3::new(origin.root, cell_x, cell_y, cell_z); 139 | let mut cell = self.cell_at(grid_point); 140 | // Temp hax? 141 | let mut rng = rand::thread_rng(); 142 | cell.shade = 1.0 - 0.5 * rng.gen::(); 143 | cells.push(cell); 144 | } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /planetkit/src/globe/icosahedron.rs: -------------------------------------------------------------------------------- 1 | // Golden ratio 2 | #[allow(clippy::unreadable_literal, clippy::excessive_precision)] 3 | const PHI: f64 = 1.61803398874989484820458683436563811772030917980576286213544862270526046281890244970720720418939113748475; 4 | 5 | // Scale factor to get icosahedron with circumscribed sphere of radius 1 6 | #[allow(clippy::unreadable_literal, clippy::excessive_precision)] 7 | const SF: f64 = 0.525731112119133606025669084847876607285497932243341781528935523241211146403214018371632628831552570956698521400021; 8 | 9 | // Vertices of an icosahedron with circumscribed sphere of radius 1 10 | // REVISIT: consider precalculating these numbers instead of basing them on the values above. 11 | const A: f64 = SF; 12 | const B: f64 = PHI * SF; 13 | #[rustfmt::skip] 14 | pub const VERTICES: [[f64; 3]; 12] = [ 15 | [ 0.0, A, B ], 16 | [ 0.0, -A, B ], 17 | [ 0.0, -A, -B ], 18 | [ 0.0, A, -B ], 19 | [ A, B, 0.0], 20 | [-A, B, 0.0], 21 | [-A, -B, 0.0], 22 | [ A, -B, 0.0], 23 | [ B, 0.0, A ], 24 | [-B, 0.0, A ], 25 | [-B, 0.0, -A ], 26 | [ B, 0.0, -A ], 27 | ]; 28 | 29 | // TODO: describe the very specific and deliberate 30 | // order that these faces are in. 31 | #[rustfmt::skip] 32 | pub const FACES: [[usize; 3]; 20] = [ 33 | [ 0, 1, 8 ], 34 | [ 7, 8, 1 ], 35 | [ 8, 7, 11 ], 36 | [ 2, 11, 7 ], 37 | 38 | [ 0, 8, 4 ], 39 | [ 11, 4, 8 ], 40 | [ 4, 11, 3 ], 41 | [ 2, 3, 11 ], 42 | 43 | [ 0, 4, 5 ], 44 | [ 3, 5, 4 ], 45 | [ 5, 3, 10 ], 46 | [ 2, 10, 3 ], 47 | 48 | [ 0, 5, 9 ], 49 | [ 10, 9, 5 ], 50 | [ 9, 10, 6 ], 51 | [ 2, 6, 10 ], 52 | 53 | [ 0, 9, 1 ], 54 | [ 6, 1, 9 ], 55 | [ 1, 6, 7 ], 56 | [ 2, 7, 6 ], 57 | ]; 58 | -------------------------------------------------------------------------------- /planetkit/src/globe/spec.rs: -------------------------------------------------------------------------------- 1 | use crate::types::*; 2 | 3 | use crate::grid::{GridCoord, Point2, Point3}; 4 | 5 | // Contains the specifications (dimensions, seed, etc.) 6 | // needed to deterministically generate a `Globe`. 7 | // 8 | // Provides helper functions that don't need to know anything 9 | // beyond these values. 10 | // 11 | // TODO: accessors for all the fields, and make them private. 12 | // 13 | // TODO: split out parameters that are applicable to all 14 | // kinds of globes, and those specific to individual kinds 15 | // of globes. 16 | #[derive(Clone, Copy)] 17 | pub struct Spec { 18 | pub seed: u64, 19 | // `rand` consumes PRNG seeds from a `[u8; 32]`, 20 | // so we store it as that (big-endian) as well for convenience. 21 | pub seed_as_u8_array: [u8; 32], 22 | pub floor_radius: f64, 23 | // NOTE: Don't let ocean radius be a neat multiple of block 24 | // height above floor radius, or we'll end up with 25 | // z-fighting in evaluating what blocks are water/air. 26 | pub ocean_radius: f64, 27 | pub block_height: f64, 28 | // These are the full width/height/depth of a given root quad or chunk's voxmap; 29 | // i.e. not an exponent. Only chunks specify a depth resolution because the 30 | // world can have unbounded total depth. 31 | pub root_resolution: [GridCoord; 2], 32 | pub chunk_resolution: [GridCoord; 3], 33 | } 34 | 35 | impl Spec { 36 | // TODO: Replace with builder pattern. 37 | pub fn new( 38 | // TODO: bump up to u128 when `bytes` releases 39 | // support for that. 40 | seed: u64, 41 | floor_radius: f64, 42 | ocean_radius: f64, 43 | block_height: f64, 44 | root_resolution: [GridCoord; 2], 45 | chunk_resolution: [GridCoord; 3], 46 | ) -> Spec { 47 | // Convert seed to `u8` array for convenience when providing 48 | // it to `rand`. 49 | use bytes::{BigEndian, ByteOrder}; 50 | let mut seed_as_u8_array = [0u8; 32]; 51 | // TODO: Just writing the seed four times for now; 52 | // will use an actual `u128` seed when `bytes` supports 53 | // writing that, and write it out twice here. 54 | BigEndian::write_u64(&mut seed_as_u8_array[0..8], seed); 55 | BigEndian::write_u64(&mut seed_as_u8_array[8..16], seed); 56 | BigEndian::write_u64(&mut seed_as_u8_array[16..24], seed); 57 | BigEndian::write_u64(&mut seed_as_u8_array[24..32], seed); 58 | 59 | Spec { 60 | seed, 61 | seed_as_u8_array, 62 | floor_radius, 63 | ocean_radius, 64 | block_height, 65 | root_resolution, 66 | chunk_resolution, 67 | } 68 | } 69 | 70 | pub fn new_earth_scale_example() -> Spec { 71 | // TODO: This only coincidentally puts you on land. 72 | // Implement deterministic (PRNG) land finding so that the seed does not matter. 73 | // 74 | // ^^ TODO: Is this still true? I think this is actually implemented now. 75 | let seed = 14; 76 | 77 | let ocean_radius = 6_371_000.0; 78 | // TODO: actually more like 60_000 when we know how to: 79 | // - Unload chunks properly 80 | // - Start with a guess about the z-position of the player 81 | // so we don't have to start at bedrock and search up. 82 | let crust_depth = 60.0; 83 | let floor_radius = ocean_radius - crust_depth; 84 | Spec::new( 85 | seed, 86 | floor_radius, 87 | ocean_radius, 88 | 0.65, 89 | [8_388_608, 16_777_216], 90 | // Chunks should probably be taller, but short chunks are a bit 91 | // better for now in exposing bugs visually. 92 | [16, 16, 4], 93 | ) 94 | } 95 | 96 | pub fn is_valid(&self) -> bool { 97 | // Chunk resolution needs to divide perfectly into root resolution. 98 | let cprs = self.chunks_per_root_side(); 99 | let calculated_root_resolution = [ 100 | cprs[0] * self.chunk_resolution[0], 101 | cprs[1] * self.chunk_resolution[1], 102 | ]; 103 | if calculated_root_resolution != self.root_resolution { 104 | return false; 105 | } 106 | 107 | // Root resolution needs to be exactly twice in the y-direction 108 | // that it is in the x-direction. I can't think of any serious 109 | // use cases for anything else, and it's extremely unclear how 110 | // a lot of scenarios should work otherwise. 111 | if self.root_resolution[1] != self.root_resolution[0] * 2 { 112 | return false; 113 | } 114 | 115 | true 116 | } 117 | 118 | pub fn chunks_per_root_side(&self) -> [GridCoord; 2] { 119 | // Assume chunk resolution divides perfectly into root resolution. 120 | [ 121 | self.root_resolution[0] / self.chunk_resolution[0], 122 | self.root_resolution[1] / self.chunk_resolution[1], 123 | ] 124 | } 125 | 126 | // Ignore the z-coordinate; just project to a unit sphere. 127 | // This is useful for, e.g., sampling noise to determine elevation 128 | // at a particular point on the surface, or other places where you're 129 | // really just talking about longitude/latitude. 130 | pub fn cell_center_on_unit_sphere(&self, column: Point2) -> Pt3 { 131 | let res_x = self.root_resolution[0] as f64; 132 | let res_y = self.root_resolution[1] as f64; 133 | let pt_in_root_quad = Pt2::new(column.x as f64 / res_x, column.y as f64 / res_y); 134 | super::project(column.root, pt_in_root_quad) 135 | } 136 | 137 | pub fn cell_center_center(&self, grid_point: Point3) -> Pt3 { 138 | let radius = self.floor_radius + self.block_height * (grid_point.z as f64 + 0.5); 139 | radius * self.cell_center_on_unit_sphere(grid_point.rxy) 140 | } 141 | 142 | pub fn cell_bottom_center(&self, grid_point: Point3) -> Pt3 { 143 | let radius = self.floor_radius + self.block_height * (grid_point.z as f64); 144 | radius * self.cell_center_on_unit_sphere(grid_point.rxy) 145 | } 146 | 147 | // TODO: describe meaning of offsets, where to get it from, etc.? 148 | pub fn cell_vertex_on_unit_sphere(&self, grid_point: Point3, offset: [i64; 2]) -> Pt3 { 149 | let res_x = (self.root_resolution[0] * 6) as f64; 150 | let res_y = (self.root_resolution[1] * 6) as f64; 151 | let pt_in_root_quad = Pt2::new( 152 | (grid_point.x as i64 * 6 + offset[0]) as f64 / res_x, 153 | (grid_point.y as i64 * 6 + offset[1]) as f64 / res_y, 154 | ); 155 | super::project(grid_point.root, pt_in_root_quad) 156 | } 157 | 158 | pub fn cell_bottom_vertex(&self, grid_point: Point3, offset: [i64; 2]) -> Pt3 { 159 | let radius = self.floor_radius + self.block_height * grid_point.z as f64; 160 | radius * self.cell_vertex_on_unit_sphere(grid_point, offset) 161 | } 162 | 163 | pub fn cell_top_vertex(&self, mut grid_point: Point3, offset: [i64; 2]) -> Pt3 { 164 | // The top of one cell is the bottom of the next. 165 | grid_point.z += 1; 166 | self.cell_bottom_vertex(grid_point, offset) 167 | } 168 | 169 | // TODO: test me. 170 | pub fn approx_cell_z_from_radius(&self, radius: f64) -> GridCoord { 171 | ((radius - self.floor_radius) / self.block_height) as GridCoord 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /planetkit/src/globe/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn find_spawn_points() { 5 | use rand; 6 | 7 | // Don't bother seeding it off the world spec/seed for now; 8 | // we just want to check that if we keep finding new points 9 | // then some of them will fail, because they are underwater. 10 | let mut rng = rand::thread_rng(); 11 | const TRIALS: usize = 200; 12 | let successes = (0..TRIALS) 13 | .map(|_| { 14 | // TODO: move globe out of closure and "collect garbage" chunks 15 | // on each iteration? Need to get into the habit of doing this. 16 | // Maybe also make globe or something record stats on how many 17 | // chunks loaded over time. 18 | let mut globe = Globe::new_earth_scale_example(); 19 | globe.air_above_random_surface_dry_land( 20 | &mut rng, 5, // Min air cells above 21 | 5, // Max distance from starting point 22 | 3, // Max attempts 23 | ) 24 | }) 25 | .filter(Option::is_some) 26 | .count(); 27 | // Some should eventually succeed, some should give up. 28 | assert!(successes > 5); 29 | assert!(successes < TRIALS - 5); 30 | } 31 | 32 | #[cfg(feature = "nightly")] 33 | pub mod benches { 34 | use test::Bencher; 35 | 36 | use slog; 37 | 38 | use super::super::*; 39 | 40 | use crate::globe::Point3; 41 | 42 | #[bench] 43 | // # History for picking the "middle of the vector" chunk. 44 | // 45 | // - Original `cull_more_faces_impractically_slow` culling implementation: 46 | // - 3,727,934 ns/iter (+/- 391,582 47 | // - After introducing `Cursor`: 48 | // - 3,618,305 ns/iter (+/- 539,063) 49 | // - No noticeable change; build_chunk_geometry already only operates 50 | // directly on a single chunk. It's the implementation of `cull_cell`, 51 | // and the underlying implementation of `Neighbors` that make it so 52 | // horrendously slow at the moment. 53 | // - After using `Cursor` in `cull_cell`: 54 | // - 861,702 ns/iter (+/- 170,677) 55 | // - Substantially better, but there are many more gains to be had. 56 | // - After cleaning up implementation and use of `Neighbors`: 57 | // - 565,896 ns/iter (+/- 237,193 58 | // - A little bit better, but mostly by eliminating completely useless 59 | // checks for diagonal neighbors. The next wins will come from implementing 60 | // an "easy case" version of `Neighbors` that avoids most of the math. 61 | // - After implementing fast `Neighbors`: 62 | // - 486,945 ns/iter (+/- 112,408) 63 | // - Only a tiny speed-up. There's lots more room for improvement on this front, 64 | // but given that my chunks at the moment are very small, I'm just going to 65 | // leave it as is and move on to bigger fish. 66 | // - After replacing chunk vector with hash map: 67 | // - 426,929 ns/iter (+/- 56,074) 68 | // - Again, only a very small improvement. This change wasn't made to speed 69 | // up generating chunk geometry; I'm just updating this history for completeness. 70 | // - I believe this change would have been a lot more noticeable if I hadn't 71 | // already implemented `Cursor`, because it speeds up looking up a chunk 72 | // by its origin, which `Cursor` helps you avoid most of the time. 73 | fn bench_generate_chunk_geometry(b: &mut Bencher) { 74 | use crate::render::Vertex; 75 | 76 | const ROOT_RESOLUTION: [GridCoord; 2] = [32, 64]; 77 | const CHUNK_RESOLUTION: [GridCoord; 3] = [16, 16, 4]; 78 | 79 | let drain = slog::Discard; 80 | let log = slog::Logger::root(drain, o!("pk_version" => env!("CARGO_PKG_VERSION"))); 81 | let spec = Spec::new(13, 0.91, 1.13, 0.02, ROOT_RESOLUTION, CHUNK_RESOLUTION); 82 | let globe = Globe::new(spec); 83 | let spec = globe.spec(); 84 | let globe_view = View::new(spec, &log); 85 | let mut vertex_data: Vec = Vec::new(); 86 | let mut index_data: Vec = Vec::new(); 87 | // Copied from output of old version of test to make sure 88 | // we're actually benchmarking the same thing. 89 | let middle_chunk_origin = ChunkOrigin::new( 90 | Point3::new(2.into(), 16, 16, 8), 91 | ROOT_RESOLUTION, 92 | CHUNK_RESOLUTION, 93 | ); 94 | b.iter(|| { 95 | vertex_data.clear(); 96 | index_data.clear(); 97 | globe_view.make_chunk_geometry( 98 | &globe, 99 | middle_chunk_origin, 100 | &mut vertex_data, 101 | &mut index_data, 102 | ); 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /planetkit/src/input_adapter.rs: -------------------------------------------------------------------------------- 1 | use piston::input::Input; 2 | 3 | /// Handles Piston input events and dispatches them to systems. 4 | pub trait InputAdapter { 5 | fn handle(&self, input_event: &Input); 6 | } 7 | -------------------------------------------------------------------------------- /planetkit/src/integration_tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod random_walk; 2 | -------------------------------------------------------------------------------- /planetkit/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # PlanetKit 3 | 4 | **PlanetKit** is game programming library with a strong focus on: 5 | 6 | - Mutable voxel-based planets 7 | - Arbitrarily large universes 8 | - Modularity and composability. 9 | 10 | It is intended as a high-level "batteries-included" toolkit for a relatively narrow set of game styles. 11 | 12 | 13 | # Project status 14 | 15 | The project is very young, and in a state of rapid flux. 16 | 17 | The API is far from stable, and documentation is sparse at best. In lieu of API stability, 18 | if you do use PlanetKit for anything, I'll do my best to help you deal with the inevitable breakage. 19 | 20 | I intend to publish the library to [crates.io](https://crates.io/) as soon as I have a token example game 21 | that uses PlanetKit as any other application would. At the moment, my example application and the 22 | library are too tangled for me to honestly call it a library ready for any kind of third party use. 23 | 24 | 25 | ## High-level design 26 | 27 | PlanetKit's architecture is based on the [entity-component system](https://en.wikipedia.org/wiki/Entity%E2%80%93component%E2%80%93system) 28 | pattern, and uses the [Specs](https://slide-rs.github.io/specs/specs/index.html) crate to implement this. Therefore the 29 | primary means of extending PlanetKit and composing different components written for it is through the use of Specs 30 | [`Component`s](https://slide-rs.github.io/specs/specs/trait.Component.html) and [`System`s](https://slide-rs.github.io/specs/specs/trait.System.html). 31 | 32 | I am keeping a close eye on [Froggy](https://github.com/kvark/froggy) as a potential replacement for Specs further down 33 | the line. This would imply significant API breakage. 34 | */ 35 | 36 | // Hook up Clippy plugin if explicitly requested. 37 | // You should only do this on nightly Rust. 38 | #![cfg_attr(feature = "clippy", feature(plugin))] 39 | #![cfg_attr(feature = "clippy", plugin(clippy))] 40 | #![cfg_attr(all(feature = "nightly", test), feature(test))] 41 | 42 | #[macro_use] 43 | extern crate gfx; 44 | #[macro_use] 45 | extern crate slog; 46 | #[macro_use] 47 | extern crate shred_derive; 48 | #[macro_use] 49 | extern crate itertools; 50 | #[macro_use] 51 | extern crate serde_derive; 52 | 53 | #[cfg(all(feature = "nightly", test))] 54 | extern crate test; 55 | #[cfg(test)] 56 | #[macro_use] 57 | extern crate approx; 58 | 59 | use nalgebra as na; 60 | 61 | pub use grid::movement; 62 | pub use planetkit_grid as grid; 63 | 64 | pub mod app; 65 | pub mod camera; 66 | pub mod cell_dweller; 67 | pub mod globe; 68 | pub mod input_adapter; 69 | pub mod net; 70 | pub mod physics; 71 | pub mod render; 72 | pub mod simple; 73 | pub mod types; 74 | pub mod window; 75 | 76 | mod spatial; 77 | pub use crate::spatial::Spatial; 78 | pub use crate::spatial::SpatialStorage; 79 | 80 | mod log_resource; 81 | pub use crate::log_resource::LogResource; 82 | 83 | mod app_builder; 84 | pub use crate::app_builder::AppBuilder; 85 | 86 | #[cfg(test)] 87 | mod integration_tests; 88 | -------------------------------------------------------------------------------- /planetkit/src/log_resource.rs: -------------------------------------------------------------------------------- 1 | use slog::Logger; 2 | 3 | pub struct LogResource { 4 | pub log: Logger, 5 | } 6 | 7 | impl LogResource { 8 | // Can't implement Default because it needs a 9 | // root logger provided from the outside world. 10 | pub fn new(parent_log: &Logger) -> LogResource { 11 | LogResource { 12 | log: parent_log.new(o!()), 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /planetkit/src/net/new_peer_system.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::sync::mpsc::TryRecvError; 3 | 4 | use slog::Logger; 5 | use specs; 6 | use specs::Write; 7 | 8 | use super::{GameMessage, NetworkPeer, NetworkPeers, NewPeer, PeerId}; 9 | 10 | pub struct NewPeerSystem { 11 | _log: Logger, 12 | new_peer_rx: std::sync::mpsc::Receiver>, 13 | } 14 | 15 | impl NewPeerSystem 16 | where 17 | G: GameMessage, 18 | { 19 | pub fn new(parent_log: &Logger, world: &mut specs::World) -> NewPeerSystem { 20 | // Take channel end we need from ServerResource. 21 | use super::ServerResource; 22 | let server_resource = world.write_resource::>(); 23 | let new_peer_rx = server_resource 24 | .new_peer_rx 25 | .lock() 26 | .expect("Couldn't get lock on new peer receiver") 27 | .take() 28 | .expect("Somebody already took it!"); 29 | 30 | NewPeerSystem { 31 | _log: parent_log.new(o!()), 32 | new_peer_rx, 33 | } 34 | } 35 | } 36 | 37 | impl<'a, G> specs::System<'a> for NewPeerSystem 38 | where 39 | G: GameMessage, 40 | { 41 | type SystemData = (Write<'a, NetworkPeers>,); 42 | 43 | fn run(&mut self, data: Self::SystemData) { 44 | let (mut network_peers,) = data; 45 | 46 | // Register any new peers that have connected 47 | // (or that we've connected to). 48 | loop { 49 | match self.new_peer_rx.try_recv() { 50 | Ok(new_peer) => { 51 | // Peer ID 0 refers to self, and isn't in the array. 52 | let next_peer_id = PeerId(network_peers.peers.len() as u16 + 1); 53 | let peer = NetworkPeer { 54 | id: next_peer_id, 55 | tcp_sender: new_peer.tcp_sender, 56 | socket_addr: new_peer.socket_addr, 57 | }; 58 | network_peers.peers.push(peer); 59 | 60 | // Cool, we've registered the peer, so we can now 61 | // handle messages from the network. Let the network 62 | // bits know that. 63 | new_peer 64 | .ready_to_receive_tx 65 | .send(()) 66 | .expect("Receiver hung up?"); 67 | 68 | // Leave a note about the new peer so game-specific 69 | // systems can do whatever initialization they might 70 | // need to do. 71 | // 72 | // TODO: don't do this until we've heard from the peer 73 | // that they are ready to receive messages. Otherwise 74 | // we might start sending them things over UDP that 75 | // they're not ready to receive, and they'll spew a bunch 76 | // of unnecessary warnings. :) 77 | network_peers.new_peers.push_back(next_peer_id); 78 | } 79 | Err(err) => { 80 | match err { 81 | TryRecvError::Empty => { 82 | break; 83 | } 84 | TryRecvError::Disconnected => { 85 | // TODO: don't panic; we're going to need 86 | // a way to shut the server down gracefully. 87 | panic!("Sender hung up"); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /planetkit/src/net/recv_system.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc; 2 | 3 | use slog::Logger; 4 | use specs; 5 | use specs::{Read, Write}; 6 | 7 | use super::{ 8 | GameMessage, NetworkPeers, RecvMessage, RecvMessageQueue, RecvWireMessage, WireMessage, 9 | }; 10 | 11 | pub struct RecvSystem { 12 | log: Logger, 13 | // Channel for slurping wire messages from network server. 14 | recv_rx: mpsc::Receiver>, 15 | } 16 | 17 | impl RecvSystem 18 | where 19 | G: GameMessage, 20 | { 21 | pub fn new(parent_log: &Logger, world: &mut specs::World) -> RecvSystem { 22 | // Take wire message receiver from ServerResource. 23 | use super::ServerResource; 24 | let server_resource = world.write_resource::>(); 25 | let recv_rx = server_resource 26 | .recv_rx 27 | .lock() 28 | .expect("Couldn't get lock on wire message receiver") 29 | .take() 30 | .expect("Somebody already took it!"); 31 | 32 | RecvSystem { 33 | log: parent_log.new(o!()), 34 | recv_rx, 35 | } 36 | } 37 | } 38 | 39 | impl<'a, G> specs::System<'a> for RecvSystem 40 | where 41 | G: GameMessage, 42 | { 43 | type SystemData = (Write<'a, RecvMessageQueue>, Read<'a, NetworkPeers>); 44 | 45 | fn run(&mut self, data: Self::SystemData) { 46 | let (mut recv_message_queue, network_peers) = data; 47 | 48 | // Slurp everything the server sent us. 49 | loop { 50 | let recv_wire_message = match self.recv_rx.try_recv() { 51 | Ok(recv_wire_message) => recv_wire_message, 52 | Err(mpsc::TryRecvError::Empty) => break, 53 | Err(mpsc::TryRecvError::Disconnected) => panic!("sender hung up"), 54 | }; 55 | 56 | let src = recv_wire_message.src; 57 | let message = match recv_wire_message.message { 58 | Ok(message) => message, 59 | Err(_) => { 60 | warn!(self.log, "Got garbled message"; "peer_addr" => format!("{:?}", src)); 61 | continue; 62 | } 63 | }; 64 | 65 | // Figure out who sent it. Do this after decoding the body so we 66 | // can log some useful information about what was in the message. 67 | // 68 | // TODO: ruh roh, what if two clients connect from the same IP? 69 | // We need to make peers always identify themselves in every message, 70 | // (and then use the HMAC to validate identity and message). 71 | let peer_id = match network_peers 72 | .peers 73 | .iter() 74 | .find(|peer| peer.socket_addr == src) 75 | { 76 | Some(peer) => peer.id, 77 | None => { 78 | warn!(self.log, "Got message from address we don't recognise; did they disconnect"; "peer_addr" => format!("{:?}", src), "message" => format!("{:?}", message)); 79 | continue; 80 | } 81 | }; 82 | 83 | let game_message = match message { 84 | WireMessage::Game(game_message) => game_message, 85 | _ => { 86 | warn!( 87 | self.log, 88 | "Don't yet know how to do anything with non-game messages" 89 | ); 90 | continue; 91 | } 92 | }; 93 | 94 | // TODO: Verify authenticity of message sender. 95 | // (All messages sent over the wire should include this, 96 | // initially as a plain assertion of their identity, and eventually 97 | // at least HMAC.) 98 | 99 | // Re-wrap the message for consumption by other systems. 100 | let recv_message = RecvMessage { 101 | source: peer_id, 102 | game_message, 103 | }; 104 | recv_message_queue.queue.push_back(recv_message); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /planetkit/src/net/send_system.rs: -------------------------------------------------------------------------------- 1 | use futures; 2 | use slog::Logger; 3 | use specs; 4 | use specs::{Read, Write}; 5 | 6 | use super::{ 7 | Destination, GameMessage, NetworkPeer, NetworkPeers, NodeResource, PeerId, RecvMessage, 8 | RecvMessageQueue, SendMessageQueue, SendWireMessage, Transport, WireMessage, 9 | }; 10 | 11 | pub struct SendSystem { 12 | log: Logger, 13 | send_udp_tx: futures::sync::mpsc::Sender>, 14 | } 15 | 16 | impl SendSystem 17 | where 18 | G: GameMessage, 19 | { 20 | pub fn new(parent_log: &Logger, world: &mut specs::World) -> SendSystem { 21 | // Take channel end we need from ServerResource. 22 | use super::ServerResource; 23 | let server_resource = world.write_resource::>(); 24 | let send_udp_tx = server_resource.send_udp_tx.clone(); 25 | 26 | SendSystem { 27 | log: parent_log.new(o!()), 28 | send_udp_tx, 29 | } 30 | } 31 | 32 | fn send_message( 33 | &mut self, 34 | game_message: G, 35 | dest_peer: &mut NetworkPeer, 36 | transport: Transport, 37 | ) { 38 | // Decide whether the message should go over TCP or UDP. 39 | match transport { 40 | Transport::UDP => { 41 | // Look up the destination socket address for this peer. 42 | // (Peer ID 0 refers to self, and isn't in the vec.) 43 | let dest_socket_addr = dest_peer.socket_addr; 44 | 45 | // Re-wrap the message for sending. 46 | let send_wire_message = SendWireMessage { 47 | dest: dest_socket_addr, 48 | message: WireMessage::Game(game_message), 49 | }; 50 | 51 | self.send_udp_tx 52 | .try_send(send_wire_message) 53 | .unwrap_or_else(|err| { 54 | error!(self.log, "Could send message to UDP client; was the buffer full?"; "err" => format!("{:?}", err)); 55 | }); 56 | } 57 | Transport::TCP => { 58 | // Look up TCP sender channel for this peer. 59 | // (Peer ID 0 refers to self, and isn't in the vec.) 60 | let sender = &mut dest_peer.tcp_sender; 61 | 62 | let wire_message = WireMessage::Game(game_message); 63 | sender.try_send(wire_message).unwrap_or_else(|err| { 64 | error!(self.log, "Could send message to TCP client; was the buffer full?"; "err" => format!("{:?}", err)); 65 | }); 66 | } 67 | } 68 | } 69 | } 70 | 71 | impl<'a, G> specs::System<'a> for SendSystem 72 | where 73 | G: GameMessage, 74 | { 75 | type SystemData = ( 76 | Write<'a, SendMessageQueue>, 77 | Write<'a, RecvMessageQueue>, 78 | Write<'a, NetworkPeers>, 79 | Read<'a, NodeResource>, 80 | ); 81 | 82 | fn run(&mut self, data: Self::SystemData) { 83 | let (mut send_message_queue, mut recv_message_queue, mut network_peers, node_resource) = 84 | data; 85 | 86 | // Send everything in send queue to UDP/TCP server. 87 | while let Some(message) = send_message_queue.queue.pop_front() { 88 | // Re-wrap message and send it to its destination(s). 89 | match message.destination { 90 | Destination::One(peer_id) => { 91 | // If the destination is ourself, 92 | // then just put it straight back on the recv 93 | // message queue. This is useful because being 94 | // able to treat yourself as just another client/peer 95 | // sometimes allows for more general code, rather than 96 | // specialising between client or server case. 97 | if peer_id.0 == 0 { 98 | recv_message_queue.queue.push_back(RecvMessage { 99 | source: peer_id, 100 | game_message: message.game_message, 101 | }); 102 | } else { 103 | self.send_message( 104 | message.game_message, 105 | &mut network_peers.peers[peer_id.0 as usize - 1], 106 | message.transport, 107 | ); 108 | } 109 | } 110 | Destination::EveryoneElse => { 111 | for peer in network_peers.peers.iter_mut() { 112 | self.send_message(message.game_message.clone(), peer, message.transport); 113 | } 114 | } 115 | Destination::EveryoneElseExcept(peer_id) => { 116 | // Everyone except yourself and another specified peer. 117 | // Useful if we just got an update (vs. polite request) from 118 | // a client that we don't intend to challenge (e.g. "I moved here" ... "ok"), 119 | // and just want to forward on to all other clients. 120 | for peer in network_peers.peers.iter_mut() { 121 | if peer.id == peer_id || peer.id.0 == 0 { 122 | continue; 123 | } 124 | self.send_message(message.game_message.clone(), peer, message.transport); 125 | } 126 | } 127 | Destination::Master => { 128 | if node_resource.is_master { 129 | recv_message_queue.queue.push_back(RecvMessage { 130 | source: PeerId(0), 131 | game_message: message.game_message, 132 | }); 133 | } else { 134 | for peer in network_peers.peers.iter_mut() { 135 | self.send_message( 136 | message.game_message.clone(), 137 | peer, 138 | message.transport, 139 | ); 140 | } 141 | } 142 | } 143 | Destination::EveryoneIncludingSelf => { 144 | // Send to everyone else. 145 | for peer in network_peers.peers.iter_mut() { 146 | self.send_message(message.game_message.clone(), peer, message.transport); 147 | } 148 | // Send to self. 149 | recv_message_queue.queue.push_back(RecvMessage { 150 | source: PeerId(0), 151 | game_message: message.game_message, 152 | }); 153 | } 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /planetkit/src/net/server.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::net::SocketAddr; 3 | use std::thread; 4 | 5 | use futures; 6 | use slog; 7 | use tokio_core::reactor::{Core, Remote}; 8 | 9 | use super::{GameMessage, NewPeer, RecvWireMessage, SendWireMessage}; 10 | 11 | /// Network client/server. 12 | /// 13 | /// Makes connections over TCP, and sends/listens 14 | /// on UDP. 15 | pub struct Server { 16 | remote: Remote, 17 | log: slog::Logger, 18 | recv_system_sender: std::sync::mpsc::Sender>, 19 | send_system_new_peer_sender: std::sync::mpsc::Sender>, 20 | // Only exists until used to start UDP server. 21 | send_udp_wire_message_rx: Option>>, 22 | // Server port, if listening. 23 | // TODO: put into a ServerState enum or something. 24 | pub port: Option, 25 | } 26 | 27 | impl Server { 28 | // TODO: require deciding up-front whether to listen on TCP, 29 | // or be a "pure client"? 30 | pub fn new( 31 | parent_logger: &slog::Logger, 32 | recv_system_sender: std::sync::mpsc::Sender>, 33 | send_system_new_peer_sender: std::sync::mpsc::Sender>, 34 | send_udp_wire_message_rx: futures::sync::mpsc::Receiver>, 35 | ) -> Server { 36 | // Run reactor on its own thread. 37 | let (remote_tx, remote_rx) = std::sync::mpsc::channel::(); 38 | thread::Builder::new() 39 | .name("network_server".to_string()) 40 | .spawn(move || { 41 | let mut reactor = Core::new().expect("Failed to create reactor for network server"); 42 | remote_tx.send(reactor.remote()).expect("Receiver hung up"); 43 | reactor 44 | .run(futures::future::empty::<(), ()>()) 45 | .expect("Network server reactor failed"); 46 | }) 47 | .expect("Failed to spawn server thread"); 48 | let remote = remote_rx.recv().expect("Sender hung up"); 49 | 50 | Server { 51 | remote, 52 | log: parent_logger.new(o!()), 53 | recv_system_sender, 54 | send_system_new_peer_sender, 55 | send_udp_wire_message_rx: Some(send_udp_wire_message_rx), 56 | port: None, 57 | } 58 | } 59 | 60 | pub fn start_listen(&mut self, port: MaybePort) 61 | where 62 | MaybePort: Into>, 63 | { 64 | self.port = super::tcp::start_tcp_server( 65 | &self.log, 66 | self.recv_system_sender.clone(), 67 | self.send_system_new_peer_sender.clone(), 68 | self.remote.clone(), 69 | port, 70 | ) 71 | .into(); 72 | super::udp::start_udp_server( 73 | &self.log, 74 | self.recv_system_sender.clone(), 75 | self.send_udp_wire_message_rx 76 | .take() 77 | .expect("Somebody else took it!"), 78 | self.remote.clone(), 79 | self.port, 80 | ); 81 | } 82 | 83 | pub fn connect(&mut self, addr: SocketAddr) { 84 | let local_port = super::tcp::connect_to_server( 85 | &self.log, 86 | self.recv_system_sender.clone(), 87 | self.send_system_new_peer_sender.clone(), 88 | self.remote.clone(), 89 | addr, 90 | ); 91 | // Listen on UDP using the same port 92 | // that the TCP server bound. 93 | // (The server will send some things on that port.) 94 | super::udp::start_udp_server( 95 | &self.log, 96 | self.recv_system_sender.clone(), 97 | self.send_udp_wire_message_rx 98 | .take() 99 | .expect("Somebody else took it!"), 100 | self.remote.clone(), 101 | local_port, 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /planetkit/src/net/server_resource.rs: -------------------------------------------------------------------------------- 1 | use futures::sync::mpsc as futmpsc; 2 | use slog::Logger; 3 | use std::sync::mpsc as stdmpsc; 4 | use std::sync::{Arc, Mutex}; 5 | 6 | use super::{GameMessage, NewPeer, RecvWireMessage, SendWireMessage, Server}; 7 | 8 | pub struct ServerResource { 9 | pub server: Arc>>, 10 | // Only exists until taken by `System` that needs it. 11 | pub recv_rx: Arc>>>>, 12 | // Only exists until taken by `System` that needs it. 13 | pub new_peer_rx: Arc>>>>, 14 | // Can be cloned; keep a copy forever. 15 | pub send_udp_tx: futmpsc::Sender>, 16 | } 17 | 18 | impl ServerResource { 19 | // Can't implement Default because it needs a 20 | // root logger provided from the outside world. 21 | pub fn new(parent_log: &Logger) -> ServerResource { 22 | // Create all the various channels we'll 23 | // need to link up the `Server`, `RecvSystem`, 24 | // and `SendSystem`. 25 | let (recv_tx, recv_rx) = stdmpsc::channel::>(); 26 | let (new_peer_tx, new_peer_rx) = stdmpsc::channel::>(); 27 | // TODO: how big is reasonable? Just go unbounded? 28 | let (send_udp_tx, send_udp_rx) = futmpsc::channel::>(1000); 29 | 30 | let server = Server::::new(&parent_log, recv_tx, new_peer_tx, send_udp_rx); 31 | ServerResource { 32 | server: Arc::new(Mutex::new(server)), 33 | recv_rx: Arc::new(Mutex::new(Some(recv_rx))), 34 | new_peer_rx: Arc::new(Mutex::new(Some(new_peer_rx))), 35 | send_udp_tx, 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /planetkit/src/physics/collider.rs: -------------------------------------------------------------------------------- 1 | use nphysics3d::object::ColliderHandle; 2 | use specs; 3 | 4 | /// A rigid body simulated by nphysics. 5 | pub struct Collider { 6 | pub collider_handle: ColliderHandle, 7 | } 8 | 9 | impl Collider { 10 | pub fn new(collider_handle: ColliderHandle) -> Collider { 11 | Collider { collider_handle } 12 | } 13 | } 14 | 15 | impl specs::Component for Collider { 16 | type Storage = specs::HashMapStorage; 17 | } 18 | -------------------------------------------------------------------------------- /planetkit/src/physics/gravity_system.rs: -------------------------------------------------------------------------------- 1 | use slog::Logger; 2 | use specs; 3 | use specs::{Entities, Read, ReadStorage, WriteStorage}; 4 | 5 | use super::Mass; 6 | use super::Velocity; 7 | use crate::globe::Globe; 8 | use crate::types::*; 9 | use crate::Spatial; 10 | 11 | // TODO: Reimplement gravity in Nphysics. 12 | 13 | /// Accelerates everything with mass toward the first globe we find. 14 | // (TODO: this is horrible hacks, but works for Kaboom.) 15 | pub struct GravitySystem { 16 | _log: Logger, 17 | } 18 | 19 | impl GravitySystem { 20 | pub fn new(parent_log: &Logger) -> GravitySystem { 21 | GravitySystem { 22 | _log: parent_log.new(o!()), 23 | } 24 | } 25 | } 26 | 27 | impl<'a> specs::System<'a> for GravitySystem { 28 | type SystemData = ( 29 | Read<'a, TimeDeltaResource>, 30 | Entities<'a>, 31 | ReadStorage<'a, Spatial>, 32 | WriteStorage<'a, Velocity>, 33 | ReadStorage<'a, Mass>, 34 | ReadStorage<'a, Globe>, 35 | ); 36 | 37 | fn run(&mut self, data: Self::SystemData) { 38 | use crate::spatial::SpatialStorage; 39 | use specs::Join; 40 | 41 | let (dt, entities, spatials, mut velocities, masses, globes) = data; 42 | 43 | // For now just find the first globe, and assume that's 44 | // the one we're supposed to be accelerating towards. 45 | let globe_entity = match (&*entities, &spatials, &globes).join().next() { 46 | Some((globe_entity, _spatial, _globe)) => globe_entity, 47 | // If there's no globe yet, then just do nothing. 48 | None => return, 49 | }; 50 | 51 | for (mass_entity, _mass, velocity) in (&*entities, &masses, &mut velocities).join() { 52 | // Accelerate toward the globe. Might as well use Earth gravity for now. 53 | // Do it "backwards" because we need to strip off the mass's orientation. 54 | // 55 | let mass_from_globe = spatials 56 | .a_relative_to_b(mass_entity, globe_entity) 57 | .translation 58 | .vector; 59 | let gravity_direction = -mass_from_globe.normalize(); 60 | let acceleration = gravity_direction * 9.8 * dt.0; 61 | *velocity.local_velocity_mut() += acceleration; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /planetkit/src/physics/mass.rs: -------------------------------------------------------------------------------- 1 | use specs; 2 | 3 | /// Objects that have mass, and are therefore affected by gravity. 4 | /// 5 | /// Assumed to also be a `Velocity`. (That's the component its 6 | /// acceleration will be applied to.) 7 | pub struct Mass { 8 | // TODO: Store its actual mass? For now that is irrelevant. 9 | } 10 | 11 | impl Mass { 12 | pub fn new() -> Mass { 13 | Mass {} 14 | } 15 | } 16 | 17 | impl specs::Component for Mass { 18 | type Storage = specs::VecStorage; 19 | } 20 | 21 | impl Default for Mass { 22 | fn default() -> Self { 23 | Self::new() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /planetkit/src/physics/mod.rs: -------------------------------------------------------------------------------- 1 | // NOTE: a lot of this is going to end up getting 2 | // replaced by nphysics. But it can't hurt to have 3 | // some degenerate versions here for now, to faciliate 4 | // building higher-level bits and pieces. 5 | 6 | mod collider; 7 | mod gravity_system; 8 | mod mass; 9 | mod physics_system; 10 | mod rigid_body; 11 | mod velocity; 12 | mod velocity_system; 13 | 14 | pub use self::collider::Collider; 15 | pub use self::gravity_system::GravitySystem; 16 | pub use self::mass::Mass; 17 | pub use self::physics_system::PhysicsSystem; 18 | pub use self::rigid_body::RigidBody; 19 | pub use self::velocity::Velocity; 20 | pub use self::velocity_system::VelocitySystem; 21 | 22 | use nphysics3d::object::{BodyHandle, ColliderHandle}; 23 | use nphysics3d::world::World; 24 | use std::collections::vec_deque::VecDeque; 25 | 26 | use crate::types::*; 27 | 28 | /// `World`-global resource for nphysics `World`. 29 | pub struct WorldResource { 30 | pub world: World, 31 | } 32 | 33 | impl Default for WorldResource { 34 | fn default() -> WorldResource { 35 | WorldResource { 36 | world: World::new(), 37 | } 38 | } 39 | } 40 | 41 | // Work-around for not being able to access removed components 42 | // in Specs FlaggedStorage. This requires systems that remove 43 | // any RigidBody (etc.) components, directly or indirectly by 44 | // deleting the entity, to push an event into this channel. 45 | // 46 | // See . 47 | 48 | pub struct RemoveBodyMessage { 49 | pub handle: BodyHandle, 50 | } 51 | 52 | #[derive(Default)] 53 | pub struct RemoveBodyQueue { 54 | pub queue: VecDeque, 55 | } 56 | 57 | pub struct RemoveColliderMessage { 58 | pub handle: ColliderHandle, 59 | } 60 | 61 | #[derive(Default)] 62 | pub struct RemoveColliderQueue { 63 | pub queue: VecDeque, 64 | } 65 | -------------------------------------------------------------------------------- /planetkit/src/physics/physics_system.rs: -------------------------------------------------------------------------------- 1 | use specs; 2 | use specs::{Read, ReadStorage, Write, WriteStorage}; 3 | 4 | use super::{RigidBody, Velocity, WorldResource}; 5 | use crate::types::*; 6 | use crate::Spatial; 7 | 8 | /// Synchronises state between the Specs and nphysics worlds, 9 | /// and drives the nphysics simulation. 10 | /// 11 | /// In order, this system 12 | /// 13 | /// 1. Copies state that has been altered in the Specs world 14 | /// (e.g. velocity) into the nphysics world. 15 | /// 2. Steps the nphysics world. 16 | /// 3. Copies state from the nphysics world (e.g. position, orientation) 17 | /// back out into the Specs world. 18 | // TODO: How are we going to be communicating collision events 19 | // into Specs land? Just make everyone who cares iterate over all of them? 20 | // Or make every system register its interest in particular objects? 21 | pub struct PhysicsSystem {} 22 | 23 | impl PhysicsSystem { 24 | pub fn new() -> PhysicsSystem { 25 | PhysicsSystem {} 26 | } 27 | } 28 | 29 | #[derive(SystemData)] 30 | pub struct PhysicsSystemData<'a> { 31 | dt: Read<'a, TimeDeltaResource>, 32 | world_resource: Write<'a, WorldResource>, 33 | velocities: WriteStorage<'a, Velocity>, 34 | spatials: WriteStorage<'a, Spatial>, 35 | rigid_bodies: ReadStorage<'a, RigidBody>, 36 | remove_body_queue: Write<'a, super::RemoveBodyQueue>, 37 | remove_collider_queue: Write<'a, super::RemoveColliderQueue>, 38 | } 39 | 40 | impl<'a> specs::System<'a> for PhysicsSystem { 41 | type SystemData = PhysicsSystemData<'a>; 42 | 43 | fn run(&mut self, mut data: Self::SystemData) { 44 | use specs::Join; 45 | 46 | // NOTE: Everything here is currently using local positions; 47 | // this will only work if everything is parented on the same Globe. 48 | // TODO: Move to separate nphysics worlds per "active" Globe. 49 | // (That's probably a while away, though.) 50 | 51 | let nphysics_world = &mut data.world_resource.world; 52 | 53 | // Remove any bodies and colliders that have had their corresponding 54 | // components in Specs land removed. 55 | // 56 | // Note that we might be removing them before the 57 | // components have actually been cleaned up, so when we 58 | // look up bodies below, we need to ignore any we don't find. 59 | while let Some(message) = data.remove_body_queue.queue.pop_front() { 60 | nphysics_world.remove_bodies(&[message.handle]); 61 | } 62 | while let Some(message) = data.remove_collider_queue.queue.pop_front() { 63 | // If there was also an associated body, 64 | // this might have been implicitly removed. 65 | if nphysics_world.collider(message.handle).is_some() { 66 | nphysics_world.remove_colliders(&[message.handle]); 67 | } 68 | } 69 | 70 | // Copy all rigid body velocities into the nphysics world. 71 | for (velocity, rigid_body) in (&data.velocities, &data.rigid_bodies).join() { 72 | use nphysics3d::math::Velocity; 73 | // Component might not have been cleaned up, even if we've 74 | // already deleted the corresponding nphysics body. 75 | if let Some(body) = nphysics_world.rigid_body_mut(rigid_body.body_handle) { 76 | body.set_velocity(Velocity::new(velocity.local_velocity(), crate::na::zero())); 77 | } 78 | } 79 | 80 | // Step the `nphysics` world. 81 | nphysics_world.set_timestep(data.dt.0); 82 | nphysics_world.step(); 83 | 84 | // Copy position and velocity back out into the Specs world. 85 | for (rigid_body, spatial, velocity) in 86 | (&data.rigid_bodies, &mut data.spatials, &mut data.velocities).join() 87 | { 88 | // Component might not have been cleaned up, even if we've 89 | // already deleted the corresponding nphysics body. 90 | if let Some(body) = nphysics_world.rigid_body(rigid_body.body_handle) { 91 | spatial.set_local_transform(*body.position()); 92 | velocity.set_local_velocity(body.velocity().linear); 93 | } 94 | } 95 | } 96 | } 97 | 98 | impl Default for PhysicsSystem { 99 | fn default() -> Self { 100 | Self::new() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /planetkit/src/physics/rigid_body.rs: -------------------------------------------------------------------------------- 1 | use nphysics3d::object::BodyHandle; 2 | use specs; 3 | 4 | /// A rigid body simulated by nphysics. 5 | pub struct RigidBody { 6 | pub body_handle: BodyHandle, 7 | } 8 | 9 | impl RigidBody { 10 | pub fn new(body_handle: BodyHandle) -> RigidBody { 11 | RigidBody { body_handle } 12 | } 13 | } 14 | 15 | impl specs::Component for RigidBody { 16 | type Storage = specs::HashMapStorage; 17 | } 18 | -------------------------------------------------------------------------------- /planetkit/src/physics/velocity.rs: -------------------------------------------------------------------------------- 1 | use specs; 2 | 3 | use crate::types::*; 4 | 5 | /// Velocity relative to some parent entity. 6 | /// 7 | /// Assumed to also be a `Spatial`. (That's where its parent 8 | /// reference is stored, and there's no meaning to velocity 9 | /// without position.) 10 | pub struct Velocity { 11 | local_velocity: Vec3, 12 | } 13 | 14 | impl Velocity { 15 | pub fn new(local_velocity: Vec3) -> Velocity { 16 | Velocity { local_velocity } 17 | } 18 | 19 | pub fn local_velocity(&self) -> Vec3 { 20 | self.local_velocity 21 | } 22 | 23 | pub fn set_local_velocity(&mut self, new_local_velocity: Vec3) { 24 | self.local_velocity = new_local_velocity; 25 | } 26 | } 27 | 28 | impl<'a> Velocity { 29 | pub fn local_velocity_mut(&'a mut self) -> &'a mut Vec3 { 30 | &mut self.local_velocity 31 | } 32 | } 33 | 34 | impl specs::Component for Velocity { 35 | type Storage = specs::VecStorage; 36 | } 37 | -------------------------------------------------------------------------------- /planetkit/src/physics/velocity_system.rs: -------------------------------------------------------------------------------- 1 | use crate::na; 2 | use slog::Logger; 3 | use specs; 4 | use specs::{Read, ReadStorage, WriteStorage}; 5 | 6 | use super::Velocity; 7 | use crate::types::*; 8 | use crate::Spatial; 9 | 10 | pub struct VelocitySystem { 11 | _log: Logger, 12 | } 13 | 14 | impl VelocitySystem { 15 | pub fn new(parent_log: &Logger) -> VelocitySystem { 16 | VelocitySystem { 17 | _log: parent_log.new(o!()), 18 | } 19 | } 20 | } 21 | 22 | impl<'a> specs::System<'a> for VelocitySystem { 23 | type SystemData = ( 24 | Read<'a, TimeDeltaResource>, 25 | WriteStorage<'a, Spatial>, 26 | ReadStorage<'a, Velocity>, 27 | ); 28 | 29 | fn run(&mut self, data: Self::SystemData) { 30 | use specs::Join; 31 | let (dt, mut spatials, velocities) = data; 32 | for (spatial, velocity) in (&mut spatials, &velocities).join() { 33 | // Apply velocity to spatial. 34 | let mut local_transform = spatial.local_transform(); 35 | let translation = na::Translation3::::from(velocity.local_velocity() * dt.0); 36 | local_transform.append_translation_mut(&translation); 37 | spatial.set_local_transform(local_transform); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /planetkit/src/render/axes_mesh.rs: -------------------------------------------------------------------------------- 1 | use super::{ProtoMesh, Vertex}; 2 | use crate::globe::icosahedron; 3 | 4 | pub const GRAY: [f32; 3] = [0.5, 0.5, 0.5]; 5 | pub const RED: [f32; 3] = [1.0, 0.0, 0.0]; 6 | pub const GREEN: [f32; 3] = [0.0, 1.0, 0.0]; 7 | pub const BLUE: [f32; 3] = [0.0, 0.0, 1.0]; 8 | 9 | pub fn make_axes_mesh() -> ProtoMesh { 10 | let mut vertex_data = Vec::::new(); 11 | let mut index_vec = Vec::::new(); 12 | 13 | let spacing = 0.5; 14 | let x_spacing = [spacing, 0.0, 0.0]; 15 | let y_spacing = [0.0, spacing, 0.0]; 16 | let z_spacing = [0.0, 0.0, spacing]; 17 | 18 | add_blob(&mut vertex_data, &mut index_vec, GRAY, [0.0, 0.0, 0.0]); 19 | add_axis(&mut vertex_data, &mut index_vec, RED, x_spacing); 20 | add_axis(&mut vertex_data, &mut index_vec, GREEN, y_spacing); 21 | add_axis(&mut vertex_data, &mut index_vec, BLUE, z_spacing); 22 | 23 | ProtoMesh::new(vertex_data, index_vec) 24 | } 25 | 26 | fn add_axis( 27 | vertex_data: &mut Vec, 28 | index_vec: &mut Vec, 29 | color: [f32; 3], 30 | spacing: [f32; 3], 31 | ) { 32 | for i in 0..3 { 33 | // Space the blobs out a bit. 34 | let offset = [ 35 | (i as f32 + 1.0) * spacing[0], 36 | (i as f32 + 1.0) * spacing[1], 37 | (i as f32 + 1.0) * spacing[2], 38 | ]; 39 | add_blob(vertex_data, index_vec, color, offset); 40 | } 41 | } 42 | 43 | fn add_blob( 44 | vertex_data: &mut Vec, 45 | index_vec: &mut Vec, 46 | color: [f32; 3], 47 | offset: [f32; 3], 48 | ) { 49 | let first_vertex_index = vertex_data.len() as u32; 50 | for vertex in &icosahedron::VERTICES { 51 | vertex_data.push(Vertex::new( 52 | [ 53 | vertex[0] as f32 * 0.2 + offset[0], 54 | vertex[1] as f32 * 0.2 + offset[1], 55 | vertex[2] as f32 * 0.2 + offset[2], 56 | ], 57 | color, 58 | )); 59 | } 60 | for face in &icosahedron::FACES { 61 | index_vec.push(first_vertex_index + face[0] as u32); 62 | index_vec.push(first_vertex_index + face[1] as u32); 63 | index_vec.push(first_vertex_index + face[2] as u32); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /planetkit/src/render/default_pipeline.rs: -------------------------------------------------------------------------------- 1 | use gfx; 2 | 3 | use crate::types::Pt3; 4 | 5 | // Pretty basic pipeline currently used for terrain. 6 | // 7 | // TODO: determine how aggressively you should be trying 8 | // to re-use this for other things. I.e. what's the cost 9 | // of having lots of pipelines and switching between them. 10 | 11 | gfx_vertex_struct!(_Vertex { 12 | a_pos: [f32; 4] = "a_pos", 13 | tex_coord: [f32; 2] = "a_tex_coord", 14 | a_color: [f32; 3] = "a_color", 15 | }); 16 | 17 | pub type Vertex = _Vertex; 18 | 19 | impl Vertex { 20 | pub fn new(pos: [f32; 3], color: [f32; 3]) -> Vertex { 21 | Vertex { 22 | a_pos: [pos[0], pos[1], pos[2], 1.0], 23 | a_color: color, 24 | tex_coord: [0.0, 0.0], 25 | } 26 | } 27 | 28 | pub fn new_from_pt3(pos: Pt3, color: [f32; 3]) -> Vertex { 29 | Vertex::new([pos[0] as f32, pos[1] as f32, pos[2] as f32], color) 30 | } 31 | } 32 | 33 | gfx_pipeline!( 34 | pipe { 35 | vbuf: gfx::VertexBuffer = (), 36 | u_model_view_proj: gfx::Global<[[f32; 4]; 4]> = "u_model_view_proj", 37 | t_color: gfx::TextureSampler<[f32; 4]> = "t_color", 38 | out_color: gfx::RenderTarget = "o_color", 39 | out_depth: gfx::DepthTarget = 40 | gfx::preset::depth::LESS_EQUAL_WRITE, 41 | } 42 | ); 43 | -------------------------------------------------------------------------------- /planetkit/src/render/encoder_channel.rs: -------------------------------------------------------------------------------- 1 | use gfx; 2 | use std::sync::mpsc; 3 | 4 | // Bi-directional channel to send Encoder between game thread(s) 5 | // (as managed by Specs), and the thread owning the graphics device. 6 | pub struct EncoderChannel> { 7 | pub receiver: mpsc::Receiver>, 8 | pub sender: mpsc::Sender>, 9 | } 10 | -------------------------------------------------------------------------------- /planetkit/src/render/mesh.rs: -------------------------------------------------------------------------------- 1 | use gfx; 2 | 3 | use super::default_pipeline::pipe; 4 | use super::Vertex; 5 | 6 | pub struct Mesh { 7 | data: pipe::Data, 8 | slice: gfx::Slice, 9 | } 10 | 11 | // Allowing sibling modules to reach into semi-private parts 12 | // of the Mesh struct. 13 | pub trait MeshGuts<'a, R: gfx::Resources> { 14 | fn data(&'a self) -> &'a pipe::Data; 15 | fn data_mut(&'a mut self) -> &'a mut pipe::Data; 16 | fn slice(&'a self) -> &'a gfx::Slice; 17 | } 18 | 19 | impl<'a, R: gfx::Resources> MeshGuts<'a, R> for Mesh { 20 | fn data(&'a self) -> &'a pipe::Data { 21 | &self.data 22 | } 23 | 24 | fn data_mut(&'a mut self) -> &'a mut pipe::Data { 25 | &mut self.data 26 | } 27 | 28 | fn slice(&'a self) -> &'a gfx::Slice { 29 | &self.slice 30 | } 31 | } 32 | 33 | impl Mesh { 34 | /// Panicks if given an empty vertex or index vector. 35 | pub fn new>( 36 | factory: &mut F, 37 | vertices: Vec, 38 | // TODO: accept usize, not u32. 39 | // That kind of optimisation isn't worthwhile until you hit the video card. 40 | vertex_indices: Vec, 41 | 42 | // TODO: this stuff belongs on `render::System` at least by default; 43 | // we're unlikely to want to customise it per mesh. 44 | output_color: gfx::handle::RenderTargetView, 45 | output_stencil: gfx::handle::DepthStencilView, 46 | ) -> Mesh { 47 | // Don't allow creating empty mesh. 48 | // Back-end doesn't seem to like this, and it probably represents 49 | // a mistake if we attempt this anyway. 50 | assert!(!vertices.is_empty()); 51 | assert!(!vertex_indices.is_empty()); 52 | 53 | // Create sampler. 54 | // TODO: surely there are some sane defaults for this stuff 55 | // I can just fall back to... 56 | // TODO: What are these magic numbers? o_0 57 | use gfx::traits::FactoryExt; 58 | let texels = [[0x20, 0xA0, 0xC0, 0x00]]; 59 | let (_, texture_view) = factory 60 | .create_texture_immutable::( 61 | gfx::texture::Kind::D2(1, 1, gfx::texture::AaMode::Single), 62 | // TODO: When we actually have pixmap textures, 63 | // this is probably going to matter. 64 | gfx::texture::Mipmap::Provided, 65 | &[&texels], 66 | ) 67 | .unwrap(); 68 | let sinfo = gfx::texture::SamplerInfo::new( 69 | gfx::texture::FilterMethod::Bilinear, 70 | gfx::texture::WrapMode::Clamp, 71 | ); 72 | 73 | let index_data: &[u32] = vertex_indices.as_slice(); 74 | let (vbuf, slice) = factory.create_vertex_buffer_with_slice(&vertices, index_data); 75 | let data = pipe::Data { 76 | vbuf: vbuf.clone(), 77 | u_model_view_proj: [[0.0; 4]; 4], 78 | t_color: (texture_view, factory.create_sampler(sinfo)), 79 | out_color: output_color, 80 | out_depth: output_stencil, 81 | }; 82 | Mesh { data, slice } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /planetkit/src/render/mesh_repository.rs: -------------------------------------------------------------------------------- 1 | use std::any; 2 | 3 | use froggy; 4 | use gfx; 5 | use slog::Logger; 6 | 7 | use super::mesh::Mesh; 8 | use super::Vertex; 9 | 10 | // Hide the concrete type of the `Mesh` (specifically the graphics backend) 11 | // by use of `Any`. TODO: there has GOT to be a better way to do this. All I really 12 | // want is to be able to make a `Pointer` to a trait that `Mesh` implements, 13 | // and use that to access the `Storage` of the specific type. 14 | pub struct MeshWrapper { 15 | mesh: Box, 16 | } 17 | 18 | /// `gfx::Factory` is not `Send`, so we can't send that around 19 | /// between threads. Instead this mesh repository is shared through 20 | /// an `Arc>` and new meshes are only ever created from 21 | /// the main thread, which owns the graphics device and therefore 22 | /// also owns the factory. See `create` below for more. 23 | pub struct MeshRepository { 24 | log: Logger, 25 | mesh_storage: froggy::Storage, 26 | default_output_color_buffer: gfx::handle::RenderTargetView, 27 | default_output_stencil_buffer: gfx::handle::DepthStencilView, 28 | } 29 | 30 | impl MeshRepository { 31 | pub fn new( 32 | default_output_color_buffer: gfx::handle::RenderTargetView, 33 | default_output_stencil_buffer: gfx::handle::DepthStencilView, 34 | parent_log: &Logger, 35 | ) -> MeshRepository { 36 | MeshRepository { 37 | mesh_storage: froggy::Storage::new(), 38 | default_output_color_buffer, 39 | default_output_stencil_buffer, 40 | log: parent_log.new(o!()), 41 | } 42 | } 43 | 44 | pub fn create>( 45 | &mut self, 46 | factory: &mut F, 47 | vertexes: Vec, 48 | triangle_vertex_indexes: Vec, 49 | ) -> froggy::Pointer { 50 | let mesh = Mesh::new( 51 | factory, 52 | vertexes, 53 | triangle_vertex_indexes, 54 | self.default_output_color_buffer.clone(), 55 | self.default_output_stencil_buffer.clone(), 56 | ); 57 | self.add_mesh(mesh) 58 | } 59 | 60 | pub fn add_mesh(&mut self, mesh: Mesh) -> froggy::Pointer { 61 | trace!(self.log, "Adding mesh"); 62 | self.mesh_storage.create(MeshWrapper { 63 | mesh: Box::new(mesh), 64 | }) 65 | } 66 | 67 | /// Destroy any unused meshes by asking the `froggy::Storage` to catch 68 | /// up on its internal bookkeeping. 69 | pub fn collect_garbage(&mut self) { 70 | self.mesh_storage.sync_pending() 71 | } 72 | } 73 | 74 | impl<'a, R: gfx::Resources> MeshRepository { 75 | pub fn get_mut(&'a mut self, mesh_pointer: &froggy::Pointer) -> &'a mut Mesh { 76 | let mesh_wrapper = &mut self.mesh_storage[&mesh_pointer]; 77 | let any_mesh_with_extra_constraints = &mut *mesh_wrapper.mesh; 78 | let any_mesh = any_mesh_with_extra_constraints as &mut dyn any::Any; 79 | any_mesh 80 | .downcast_mut::>() 81 | .expect("Unless we're mixing graphics backends, this should be impossible.") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /planetkit/src/render/mod.rs: -------------------------------------------------------------------------------- 1 | mod axes_mesh; 2 | mod default_pipeline; 3 | mod encoder_channel; 4 | mod mesh; 5 | mod mesh_repository; 6 | mod proto_mesh; 7 | mod system; 8 | mod visual; 9 | 10 | pub use self::axes_mesh::make_axes_mesh; 11 | pub use self::default_pipeline::Vertex; 12 | pub use self::encoder_channel::EncoderChannel; 13 | pub use self::mesh::Mesh; 14 | pub use self::mesh_repository::{MeshRepository, MeshWrapper}; 15 | pub use self::proto_mesh::ProtoMesh; 16 | pub use self::system::System; 17 | pub use self::visual::Visual; 18 | -------------------------------------------------------------------------------- /planetkit/src/render/proto_mesh.rs: -------------------------------------------------------------------------------- 1 | use super::Vertex; 2 | 3 | #[derive(Clone)] 4 | pub struct ProtoMesh { 5 | pub vertexes: Vec, 6 | pub indexes: Vec, 7 | } 8 | 9 | impl ProtoMesh { 10 | /// Panicks if given an empty vertex or index vector. 11 | pub fn new(vertexes: Vec, indexes: Vec) -> ProtoMesh { 12 | // Don't allow creating empty mesh. 13 | // Back-end doesn't seem to like this, and it probably represents 14 | // a mistake if we attempt this anyway. 15 | assert!(!vertexes.is_empty()); 16 | assert!(!indexes.is_empty()); 17 | 18 | ProtoMesh { vertexes, indexes } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /planetkit/src/render/visual.rs: -------------------------------------------------------------------------------- 1 | use froggy; 2 | use specs; 3 | 4 | use super::MeshWrapper; 5 | use super::ProtoMesh; 6 | 7 | pub struct Visual { 8 | // Even if a component has visual nature, its mesh might 9 | // not have been created yet at the time the entity is created, 10 | // and we don't want to have to hold up the show to wait for that. 11 | // We may also want to change its appearance dynamically. 12 | mesh_pointer: Option>, 13 | // Vertex and index data that hasn't yet been sent to 14 | // the video card. Render system uses this to replace the 15 | // actual mesh whenever this is present. 16 | // TODO: privacy 17 | pub proto_mesh: Option, 18 | } 19 | 20 | impl Visual { 21 | pub fn new_empty() -> Visual { 22 | Visual { 23 | mesh_pointer: None, 24 | proto_mesh: None, 25 | } 26 | } 27 | 28 | pub fn mesh_pointer(&self) -> Option<&froggy::Pointer> { 29 | self.mesh_pointer.as_ref() 30 | } 31 | 32 | pub fn set_mesh_pointer(&mut self, new_mesh_pointer: froggy::Pointer) { 33 | self.mesh_pointer = new_mesh_pointer.into(); 34 | } 35 | } 36 | 37 | impl specs::Component for Visual { 38 | type Storage = specs::VecStorage; 39 | } 40 | -------------------------------------------------------------------------------- /planetkit/src/shaders/copypasta_150.glslf: -------------------------------------------------------------------------------- 1 | #version 150 core 2 | 3 | in vec2 v_tex_coord; 4 | in vec4 v_color; 5 | out vec4 o_color; 6 | uniform sampler2D t_color; 7 | 8 | void main() { 9 | o_color = v_color; 10 | } 11 | -------------------------------------------------------------------------------- /planetkit/src/shaders/copypasta_150.glslv: -------------------------------------------------------------------------------- 1 | #version 150 core 2 | in vec3 a_pos; 3 | in vec2 a_tex_coord; 4 | in vec3 a_color; 5 | out vec2 v_tex_coord; 6 | out vec4 v_color; 7 | uniform mat4 u_model_view_proj; 8 | void main() { 9 | v_tex_coord = a_tex_coord; 10 | v_color = vec4(a_color, 1.0); 11 | gl_Position = u_model_view_proj * vec4(a_pos, 1.0); 12 | } 13 | -------------------------------------------------------------------------------- /planetkit/src/shaders/copypasta_300_es.glslf: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision mediump float; 4 | 5 | in vec2 v_tex_coord; 6 | in vec4 v_color; 7 | out vec4 o_color; 8 | uniform sampler2D t_color; 9 | 10 | void main() { 11 | o_color = v_color; 12 | } 13 | -------------------------------------------------------------------------------- /planetkit/src/shaders/copypasta_300_es.glslv: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec3 a_pos; 4 | in vec2 a_tex_coord; 5 | in vec3 a_color; 6 | out vec2 v_tex_coord; 7 | out vec4 v_color; 8 | uniform mat4 u_model_view_proj; 9 | 10 | void main() { 11 | v_tex_coord = a_tex_coord; 12 | v_color = vec4(a_color, 1.0); 13 | gl_Position = u_model_view_proj * vec4(a_pos, 1.0); 14 | } 15 | -------------------------------------------------------------------------------- /planetkit/src/simple.rs: -------------------------------------------------------------------------------- 1 | use specs; 2 | use specs::{Builder, Entities, LazyUpdate, Read}; 3 | 4 | use crate::camera::DefaultCamera; 5 | use crate::cell_dweller; 6 | use crate::globe; 7 | use crate::render; 8 | use crate::types::*; 9 | 10 | // TODO: Retire most of this stuff. Or, rather, turn it into 11 | // a module (eventually split out into a separate crate) of easy-to-combine 12 | // bits and pieces, but still encouraging best practices. 13 | // 14 | // For example, pretty much all this stuff should be done inside 15 | // a system rather than executing outside of the normal game loop. 16 | // (So that it can interact with other systems, like networking, 17 | // load/save, etc.) 18 | 19 | pub fn populate_world(world: &mut specs::World) { 20 | let globe_entity = create_simple_globe_now(world); 21 | let player_character_entity = create_simple_player_character_now(world, globe_entity); 22 | create_simple_chase_camera_now(world, player_character_entity); 23 | } 24 | 25 | pub fn create_simple_globe_now(world: &mut specs::World) -> specs::Entity { 26 | let globe = globe::Globe::new_earth_scale_example(); 27 | world 28 | .create_entity() 29 | .with(globe) 30 | .with(crate::Spatial::new_root()) 31 | .build() 32 | } 33 | 34 | pub fn create_simple_player_character_now( 35 | world: &mut specs::World, 36 | globe_entity: specs::Entity, 37 | ) -> specs::Entity { 38 | use rand::SeedableRng; 39 | use rand_xoshiro::Xoshiro256StarStar; 40 | 41 | // Find a suitable spawn point for the player character at the globe surface. 42 | use crate::grid::Dir; 43 | let (globe_spec, player_character_pos) = { 44 | let mut globe_storage = world.write_storage::(); 45 | let globe = globe_storage 46 | .get_mut(globe_entity) 47 | .expect("Uh oh, it looks like our Globe went missing."); 48 | let globe_spec = globe.spec(); 49 | // Seed spawn point RNG with world seed. 50 | let mut rng = Xoshiro256StarStar::from_seed(globe_spec.seed_as_u8_array); 51 | let player_character_pos = globe 52 | .air_above_random_surface_dry_land( 53 | &mut rng, 2, // Min air cells above 54 | 5, // Max distance from starting point 55 | 5, // Max attempts 56 | ) 57 | .expect("Oh noes, we took too many attempts to find a decent spawn point!"); 58 | (globe_spec, player_character_pos) 59 | }; 60 | 61 | // Make visual appearance of player character. 62 | // For now this is just an axes mesh. 63 | let mut player_character_visual = render::Visual::new_empty(); 64 | player_character_visual.proto_mesh = Some(render::make_axes_mesh()); 65 | 66 | let player_character_entity = world 67 | .create_entity() 68 | .with(cell_dweller::CellDweller::new( 69 | player_character_pos, 70 | Dir::default(), 71 | globe_spec, 72 | Some(globe_entity), 73 | )) 74 | .with(player_character_visual) 75 | // The CellDweller's transformation will be set based 76 | // on its coordinates in cell space. 77 | .with(crate::Spatial::new(globe_entity, Iso3::identity())) 78 | .build(); 79 | // Set our new character as the currently controlled cell dweller. 80 | world 81 | .write_resource::() 82 | .maybe_entity = Some(player_character_entity); 83 | player_character_entity 84 | } 85 | 86 | pub fn create_simple_chase_camera_now( 87 | world: &mut specs::World, 88 | player_character_entity: specs::Entity, 89 | ) -> specs::Entity { 90 | // Create a camera sitting a little bit behind the cell dweller. 91 | let eye = Pt3::new(0.0, 4.0, -6.0); 92 | let target = Pt3::origin(); 93 | let camera_transform = Iso3::face_towards(&eye, &target, &Vec3::z()); 94 | let camera_entity = world 95 | .create_entity() 96 | .with(crate::Spatial::new( 97 | player_character_entity, 98 | camera_transform, 99 | )) 100 | .build(); 101 | use crate::camera::DefaultCamera; 102 | // TODO: gah, where does this belong? 103 | world.add_resource(DefaultCamera { 104 | camera_entity: Some(camera_entity), 105 | }); 106 | camera_entity 107 | } 108 | 109 | pub fn create_simple_chase_camera( 110 | entities: &Entities<'_>, 111 | updater: &Read<'_, LazyUpdate>, 112 | player_character_entity: specs::Entity, 113 | default_camera: &mut DefaultCamera, 114 | ) -> specs::Entity { 115 | // Create a camera sitting a little bit behind the cell dweller. 116 | let eye = Pt3::new(0.0, 4.0, -6.0); 117 | let target = Pt3::origin(); 118 | let camera_transform = Iso3::face_towards(&eye, &target, &Vec3::z()); 119 | let entity = entities.create(); 120 | updater.insert( 121 | entity, 122 | crate::Spatial::new(player_character_entity, camera_transform), 123 | ); 124 | default_camera.camera_entity = Some(entity); 125 | entity 126 | } 127 | -------------------------------------------------------------------------------- /planetkit/src/types.rs: -------------------------------------------------------------------------------- 1 | use crate::na; 2 | 3 | // TODO: Derive everything else from this. 4 | // TODO: Rename; this is too likely to end up in too 5 | // many scopes to have such a general name. 6 | // Maybe "PKFloat"? 7 | // 8 | // _OR_ just really discourage importing everything 9 | // from here, and encourage using as, e.g., `pk::Real`, 10 | // `pk::Point3`. That's probably better. Then you can also 11 | // encourage referring to the _grid_ types as, e.g., 12 | // `gg::Point3` or `grid::Point3` to avoid ambiguity. 13 | pub type Real = f64; 14 | 15 | // Common types for all of PlanetKit. 16 | // 17 | // REVISIT: should some of these actually be `f32` 18 | // for performance reasons? We definitely want 19 | // `f64` for doing the non-realtime geometric 20 | // manipulations, but entity positions etc. don't 21 | // really need it. 22 | pub type Vec2 = na::Vector2; 23 | pub type Vec3 = na::Vector3; 24 | pub type Pt2 = na::Point2; 25 | pub type Pt3 = na::Point3; 26 | pub type Rot3 = na::Rotation3; 27 | pub type Iso3 = na::Isometry3; 28 | 29 | pub type TimeDelta = f64; 30 | 31 | #[derive(Default)] 32 | pub struct TimeDeltaResource(pub TimeDelta); 33 | 34 | pub type Mat4 = na::Matrix4; 35 | -------------------------------------------------------------------------------- /planetkit/src/window.rs: -------------------------------------------------------------------------------- 1 | use glutin_window::GlutinWindow; 2 | use piston_window::PistonWindow; 3 | use slog::Logger; 4 | 5 | pub fn make_window(log: &Logger) -> PistonWindow { 6 | use opengl_graphics::OpenGL; 7 | use piston::window::AdvancedWindow; 8 | use piston::window::WindowSettings; 9 | use piston_window::PistonWindow; 10 | 11 | // Change this to OpenGL::V2_1 if not working. 12 | let opengl = OpenGL::V3_2; 13 | 14 | // Create Glutin settings. 15 | info!(log, "Creating Glutin window"); 16 | let settings = WindowSettings::new("planetkit", [800, 600]) 17 | .opengl(opengl) 18 | .exit_on_esc(true); 19 | 20 | // Create Glutin window from settings. 21 | info!(log, "Creating Glutin window"); 22 | let samples = settings.get_samples(); 23 | let glutin_window: GlutinWindow = settings.build().expect("Failed to build Glutin window"); 24 | 25 | // Create a Piston window. 26 | info!(log, "Creating main PistonWindow"); 27 | let mut window: PistonWindow = PistonWindow::new(opengl, samples, glutin_window); 28 | 29 | window.set_capture_cursor(false); 30 | debug!(log, "Main window created"); 31 | 32 | window 33 | } 34 | -------------------------------------------------------------------------------- /pre_commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | cargo test --release 6 | cargo fmt 7 | cargo clippy --bins --examples --tests --benches -- -D warnings 8 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffparsons/planetkit/67a9e3f45ae45bbb229e6dae10638d25fbece1be/screenshot.png -------------------------------------------------------------------------------- /travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd $TRAVIS_BUILD_DIR/planetkit 3 | cargo build --release --verbose 4 | cargo test --release --verbose 5 | -------------------------------------------------------------------------------- /warm_build_cache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Build everything in debug and release modes, including all tests 4 | # and benchmarks, to make sure we have all dependencies downloaded 5 | # and everything compiled. 6 | # 7 | # This is mostly useful if, e.g., you've just pulled down a new version 8 | # of the Rust compiler, and don't want surprise long compile wait times later. 9 | # (Especially useful if you're about to hit the road soon and don't 10 | # want to burn through your battery life and/or mobile data quota.) 11 | 12 | cargo build --bins --tests --benches --examples 13 | cargo build --bins --tests --benches --examples --release 14 | -------------------------------------------------------------------------------- /webdemo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webdemo" 3 | version = "0.0.1" 4 | authors = ["Jeff Parsons "] 5 | 6 | [dependencies] 7 | planetkit = { path = "../planetkit", default-features = false, features = ["web"] } 8 | stdweb = "0.2.1" 9 | -------------------------------------------------------------------------------- /webdemo/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Bold, red text 4 | tput bold 5 | tput setaf 1 6 | 7 | echo 8 | echo "This doesn't actually work yet. Don't expect it to." 9 | echo 10 | 11 | # Non-bold, normal text 12 | tput sgr0 13 | 14 | # Parse command line arguments. 15 | # `getopt` is fiddly, and overly complex for our hacky needs here. 16 | # Just pump args ignoring anything we don't recognise. 17 | while [[ ! -z $1 ]]; do 18 | if [[ $1 == '--release' ]]; then 19 | maybe_release='--release' 20 | echo "Building in release mode." 21 | shift 22 | fi 23 | 24 | if [[ $1 == '--nightly' ]]; then 25 | maybe_nightly='+nightly' 26 | echo "Building using nightly toolchain." 27 | shift 28 | fi 29 | done 30 | 31 | if [[ ! -z "${maybe_release}" ]]; then 32 | dest=target/wasm32-unknown-emscripten/release/ 33 | else 34 | dest=target/wasm32-unknown-emscripten/debug/ 35 | fi 36 | 37 | cargo $maybe_nightly build $maybe_release --target wasm32-unknown-emscripten 38 | 39 | cp index.html "$dest" 40 | pushd "$dest" 41 | echo "Trying to serve up cool stuff on http://localhost:8123..." 42 | python -m SimpleHTTPServer 8123 43 | -------------------------------------------------------------------------------- /webdemo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /webdemo/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate planetkit as pk; 2 | 3 | fn main() { 4 | let mut app = pk::AppBuilder::new() 5 | .with_common_systems() 6 | .build_gui(); 7 | pk::simple::populate_world(app.world_mut()); 8 | app.run(); 9 | } 10 | -------------------------------------------------------------------------------- /woolgather/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "woolgather" 3 | version = "0.0.1" 4 | authors = ["Jeff Parsons "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | planetkit = { path = "../planetkit" } 9 | rand = "0.6" 10 | rand_xoshiro = "0.1" 11 | shred = "0.7" 12 | specs = "0.14" 13 | slog = "2.0.4" 14 | -------------------------------------------------------------------------------- /woolgather/src/game_state.rs: -------------------------------------------------------------------------------- 1 | /// `World`-global resource for game, including any global state relating 2 | /// to the current level (start time, did you win, etc.) but also any global state 3 | /// that must persist between levels (what campaign is loaded, etc.). 4 | #[derive(Default)] 5 | pub struct GameState { 6 | pub current_level: LevelState, 7 | } 8 | 9 | pub struct LevelState { 10 | // Have we created everything for the level yet? 11 | pub initialized: bool, 12 | pub level_outcome: LevelOutcome, 13 | } 14 | 15 | impl Default for LevelState { 16 | fn default() -> LevelState { 17 | LevelState { 18 | initialized: false, 19 | level_outcome: LevelOutcome::Pending, 20 | } 21 | } 22 | } 23 | 24 | pub enum LevelOutcome { 25 | Pending, 26 | Won, 27 | _Lost, 28 | } 29 | -------------------------------------------------------------------------------- /woolgather/src/game_system.rs: -------------------------------------------------------------------------------- 1 | use slog::Logger; 2 | use specs; 3 | use specs::Write; 4 | 5 | use super::game_state::{GameState, LevelOutcome}; 6 | 7 | /// System to drive the top-level state machine for level and game state. 8 | pub struct GameSystem { 9 | logger: Logger, 10 | } 11 | 12 | impl GameSystem { 13 | pub fn new(parent_log: &Logger) -> GameSystem { 14 | GameSystem { 15 | logger: parent_log.new(o!("system" => "game")), 16 | } 17 | } 18 | } 19 | 20 | impl<'a> specs::System<'a> for GameSystem { 21 | type SystemData = (Write<'a, GameState>,); 22 | 23 | fn run(&mut self, data: Self::SystemData) { 24 | let (mut game_state,) = data; 25 | // TEMP: instant-win! 26 | match game_state.current_level.level_outcome { 27 | LevelOutcome::Pending => { 28 | game_state.current_level.level_outcome = LevelOutcome::Won; 29 | info!(self.logger, "You successfully completed the current level!"); 30 | } 31 | LevelOutcome::Won => { 32 | // Nothing can stop us now; we've already won! 33 | } 34 | LevelOutcome::_Lost => { 35 | // Nothing can save us now; we've already lost! 36 | // TODO: maybe reset the level so we can try again? 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /woolgather/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate slog; 3 | 4 | mod game_state; 5 | mod game_system; 6 | mod shepherd; 7 | 8 | use planetkit as pk; 9 | use specs; 10 | 11 | fn main() { 12 | let mut app = pk::AppBuilder::new() 13 | .with_common_systems() 14 | .with_systems(add_systems) 15 | .build_gui(); 16 | create_entities(app.world_mut()); 17 | app.run(); 18 | } 19 | 20 | fn add_systems( 21 | logger: &slog::Logger, 22 | _world: &mut specs::World, 23 | dispatcher_builder: specs::DispatcherBuilder<'static, 'static>, 24 | ) -> specs::DispatcherBuilder<'static, 'static> { 25 | let game_system = game_system::GameSystem::new(logger); 26 | dispatcher_builder.with(game_system, "woolgather_game", &[]) 27 | } 28 | 29 | fn create_entities(world: &mut specs::World) { 30 | use crate::pk::cell_dweller::ActiveCellDweller; 31 | 32 | // TODO: this should all actually be done by a game system, 33 | // rather than in the app builder. Because, e.g. if you change levels, 34 | // it needs to know how to create all this. 35 | 36 | // Create the globe first, because we'll need it to figure out where 37 | // to place the shepherd (player character). 38 | let globe_entity = pk::simple::create_simple_globe_now(world); 39 | 40 | // Create the shepherd. 41 | let shepherd_entity = shepherd::create_now(world, globe_entity); 42 | // Set our new shepherd player character as the currently controlled cell dweller. 43 | world.write_resource::().maybe_entity = Some(shepherd_entity); 44 | 45 | // Create basic third-person following camera. 46 | pk::simple::create_simple_chase_camera_now(world, shepherd_entity); 47 | } 48 | -------------------------------------------------------------------------------- /woolgather/src/shepherd.rs: -------------------------------------------------------------------------------- 1 | use crate::pk; 2 | use crate::pk::cell_dweller; 3 | use crate::pk::globe; 4 | use crate::pk::grid; 5 | use crate::pk::render; 6 | use crate::pk::types::*; 7 | use specs::{self, Builder}; 8 | 9 | /// Create the player character: a shepherd who must find and rescue the sheep 10 | /// that have strayed from his flock and fallen into holes. 11 | pub fn create_now(world: &mut specs::World, globe_entity: specs::Entity) -> specs::Entity { 12 | use rand::SeedableRng; 13 | use rand_xoshiro::Xoshiro256StarStar; 14 | 15 | // Find a suitable spawn point for the player character at the globe surface. 16 | let (globe_spec, shepherd_pos) = { 17 | let mut globe_storage = world.write_storage::(); 18 | let globe = globe_storage 19 | .get_mut(globe_entity) 20 | .expect("Uh oh, it looks like our Globe went missing."); 21 | let globe_spec = globe.spec(); 22 | // Seed spawn point RNG with world seed. 23 | let mut rng = Xoshiro256StarStar::from_seed(globe_spec.seed_as_u8_array); 24 | let shepherd_pos = globe 25 | .air_above_random_surface_dry_land( 26 | &mut rng, 2, // Min air cells above 27 | 5, // Max distance from starting point 28 | 5, // Max attempts 29 | ) 30 | .expect("Oh noes, we took too many attempts to find a decent spawn point!"); 31 | (globe_spec, shepherd_pos) 32 | }; 33 | 34 | // Make visual appearance of player character. 35 | // For now this is just an axes mesh. 36 | let mut shepherd_visual = render::Visual::new_empty(); 37 | shepherd_visual.proto_mesh = Some(render::make_axes_mesh()); 38 | 39 | let shepherd_entity = world 40 | .create_entity() 41 | .with(cell_dweller::CellDweller::new( 42 | shepherd_pos, 43 | grid::Dir::default(), 44 | globe_spec, 45 | Some(globe_entity), 46 | )) 47 | .with(shepherd_visual) 48 | // The CellDweller's transformation will be set based 49 | // on its coordinates in cell space. 50 | .with(pk::Spatial::new(globe_entity, Iso3::identity())) 51 | .build(); 52 | shepherd_entity 53 | } 54 | --------------------------------------------------------------------------------