Skip to content
This repository has been archived by the owner on Oct 25, 2021. It is now read-only.

Commit

Permalink
Add perception system with basic spatial acceleration structure (#68)
Browse files Browse the repository at this point in the history
* Add perception components
* Add skeleton of entity detection system
* Add global transform component and system to get an entity absolute position (due to possible hierarchy of entities)
* Add spatial grid system
* Add spatial acceleration to the perception system
* Use bitset for nearby entities detected
  • Loading branch information
sunreef authored and marotili committed May 21, 2019
1 parent f6c2b7e commit cbd1ce7
Show file tree
Hide file tree
Showing 16 changed files with 258 additions and 18 deletions.
3 changes: 3 additions & 0 deletions resources/prefabs/creatures/carnivore.ron
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ Prefab (
),
),
intelligence_tag: (),
perception: (
range: 5.0,
),
),
),
],
Expand Down
3 changes: 3 additions & 0 deletions resources/prefabs/creatures/herbivore.ron
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ Prefab (
),
),
intelligence_tag: (),
perception: (
range: 5.0,
),
),
),
],
Expand Down
3 changes: 3 additions & 0 deletions resources/prefabs/creatures/ixie.ron
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Prefab (
value: 10.0,
),
),
perception: (
range: 0.8,
),
),
),
],
Expand Down
2 changes: 2 additions & 0 deletions src/components/creatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};

use crate::components::{
collider::Circle, combat::CombatPrefabData, digestion::DigestionPrefabData,
perception::Perception,
};

pub type CreatureType = String;
Expand Down Expand Up @@ -82,4 +83,5 @@ pub struct CreaturePrefabData {
digestion: Option<DigestionPrefabData>,
combat: Option<CombatPrefabData>,
intelligence_tag: Option<IntelligenceTag>,
perception: Option<Perception>,
}
1 change: 1 addition & 0 deletions src/components/experimental/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod perception;
26 changes: 26 additions & 0 deletions src/components/experimental/perception.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use amethyst::{
assets::{PrefabData, PrefabError},
ecs::{Component, DenseVecStorage, Entity, WriteStorage},
};
use amethyst_inspector::Inspect;
use serde::{Deserialize, Serialize};

#[derive(Default, Clone, Debug, Inspect, Serialize, Deserialize, PrefabData)]
#[prefab(Component)]
#[serde(default)]
pub struct Perception {
pub range: f32,
}

impl Component for Perception {
type Storage = DenseVecStorage<Self>;
}

#[derive(Default, Clone, Debug)]
pub struct DetectedEntities {
pub entities: Vec<Entity>,
}

impl Component for DetectedEntities {
type Storage = DenseVecStorage<Self>;
}
3 changes: 3 additions & 0 deletions src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ pub mod combat;
pub mod creatures;
pub mod digestion;
pub mod swarm;

mod experimental;
pub use experimental::*;
1 change: 1 addition & 0 deletions src/resources/experimental/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod spatial_grid;
82 changes: 82 additions & 0 deletions src/resources/experimental/spatial_grid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use amethyst::{
core::{nalgebra::Vector4, transform::GlobalTransform},
ecs::Entity,
};

use std::collections::HashMap;
use std::f32;

// The SpatialGrid is a spatial hashing structure used to accelerate neighbor searches for entities.
pub struct SpatialGrid {
cell_size: f32,
cells: HashMap<i32, HashMap<i32, Vec<Entity>>>,
}

impl SpatialGrid {
pub fn new(cell_size: f32) -> Self {
SpatialGrid {
cell_size,
cells: HashMap::new(),
}
}

pub fn reset(&mut self) {
self.cells = HashMap::new();
}

// Insert an entity in the grid based on its GlobalTransform component.
// This might have to change when upgrading Amethyst to 0.11 as the GlobalTransform component was removed.
pub fn insert(&mut self, entity: Entity, transform: &GlobalTransform) {
let pos = Vector4::from(transform.as_ref()[3]);
let x_cell = (pos[0] / self.cell_size).floor() as i32;
let y_cell = (pos[1] / self.cell_size).floor() as i32;
let row_entry = self.cells.entry(x_cell).or_insert(HashMap::new());
let col_entry = row_entry.entry(y_cell).or_insert(Vec::new());
col_entry.push(entity);
}

// Query the entities close to a certain position.
// The range of the query is defined by the range input.
pub fn query(&self, transform: &GlobalTransform, range: f32) -> Vec<Entity> {
let pos = Vector4::from(transform.as_ref()[3]);
let x_cell = (pos[0] / self.cell_size).floor() as i32;
let y_cell = (pos[1] / self.cell_size).floor() as i32;
let integer_range = 1 + (range / self.cell_size).ceil() as i32;
let mut entities = Vec::new();
for x in -integer_range..integer_range {
for y in -integer_range..integer_range {
match self.cells.get(&(x_cell + x)) {
Some(col) => match col.get(&(y_cell + y)) {
Some(cell) => entities.extend_from_slice(cell.as_slice()),
None => (),
},
None => (),
}
}
}
entities
}
}

#[cfg(test)]
mod tests {
use super::*;
use amethyst::{
core::transform::Transform,
ecs::{Builder, World},
};

#[test]
fn grid_creation_insertion_and_query() {
let mut world = World::new();
let mut spatial_grid = SpatialGrid::new(1.0f32);

let transform = Transform::default();
let transform_matrix = transform.matrix();
let global_transform = GlobalTransform::from(*transform_matrix.as_ref());
spatial_grid.insert(world.create_entity().build(), &global_transform);

assert!(spatial_grid.query(&global_transform, 1.0f32).len() == 1);
}

}
3 changes: 3 additions & 0 deletions src/resources/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ pub mod audio;
pub mod debug;
pub mod prefabs;
pub mod world_bounds;

mod experimental;
pub use experimental::*;
15 changes: 14 additions & 1 deletion src/states/main_game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::systems::behaviors::decision::{
ClosestSystem, Predator, Prey, QueryPredatorsAndPreySystem, SeekSystem,
};
use crate::{
resources::{debug::DebugConfig, world_bounds::WorldBounds},
resources::{debug::DebugConfig, spatial_grid::SpatialGrid, world_bounds::WorldBounds},
states::{paused::PausedState, CustomStateEvent},
systems::*,
};
Expand All @@ -35,6 +35,12 @@ impl MainGameState {
MainGameState {
dispatcher: DispatcherBuilder::new()
.with_pool(pool)
.with(perception::SpatialGridSystem, "spatial_grid", &[])
.with(
perception::EntityDetectionSystem,
"entity_detection",
&["spatial_grid"],
)
.with(
QueryPredatorsAndPreySystem,
"query_predators_and_prey_system",
Expand Down Expand Up @@ -142,6 +148,11 @@ impl MainGameState {
.with(collision::DebugColliderSystem, "debug_collider_system", &[])
.with(debug::DebugSystem, "debug_system", &[])
.with(digestion::DebugFullnessSystem, "debug_fullness_system", &[])
.with(
perception::DebugEntityDetectionSystem,
"debug_entity_detection",
&[],
)
.build(),
// The ui dispatcher will also run when this game state is paused. This is necessary so that
// the user can interact with the UI even if the game is in the `Paused` game state.
Expand Down Expand Up @@ -198,6 +209,8 @@ impl<'a> State<GameData<'a, 'a>, CustomStateEvent> for MainGameState {
// Setup debug config resource
data.world.add_resource(DebugConfig::default());

data.world.add_resource(SpatialGrid::new(1.0f32));

time_control::create_time_control_ui(&mut data.world);

// Add some plants
Expand Down
1 change: 1 addition & 0 deletions src/systems/experimental/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod perception;
104 changes: 104 additions & 0 deletions src/systems/experimental/perception.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use amethyst::{
core::{nalgebra::Vector4, transform::GlobalTransform},
ecs::{
BitSet, Entities, Join, ReadExpect, ReadStorage, System, Write, WriteExpect, WriteStorage,
},
renderer::DebugLines,
};

use crate::components::perception::{DetectedEntities, Perception};
use crate::resources::spatial_grid::SpatialGrid;

pub struct EntityDetectionSystem;

impl<'s> System<'s> for EntityDetectionSystem {
type SystemData = (
Entities<'s>,
ReadStorage<'s, Perception>,
WriteStorage<'s, DetectedEntities>,
ReadExpect<'s, SpatialGrid>,
ReadStorage<'s, GlobalTransform>,
);

fn run(
&mut self,
(entities, perceptions, mut detected_entities, grid, globals): Self::SystemData,
) {
for (entity, _) in (&entities, &perceptions).join() {
match detected_entities.get(entity) {
Some(_) => (),
None => {
detected_entities
.insert(entity, DetectedEntities::default())
.expect("Unreachable, we just tested the entity exists");
}
}
}

for (entity, perception, mut detected, global) in
(&entities, &perceptions, &mut detected_entities, &globals).join()
{
detected.entities = Vec::new();
let nearby_entities = grid.query(global, perception.range);
let pos = Vector4::from(global.as_ref()[3]).xyz();
let sq_range = perception.range * perception.range;
let mut nearby_entities_bitset = BitSet::new();
for other_entity in &nearby_entities {
if entity == *other_entity {
continue;
}
nearby_entities_bitset.add(other_entity.id());
}
for (other_entity, other_global, _) in
(&entities, &globals, &nearby_entities_bitset).join()
{
let other_pos = Vector4::from(other_global.as_ref()[3]).xyz();
if (pos - other_pos).norm_squared() < sq_range {
detected.entities.push(other_entity);
}
}
}
}
}

pub struct SpatialGridSystem;

impl<'s> System<'s> for SpatialGridSystem {
type SystemData = (
Entities<'s>,
ReadStorage<'s, GlobalTransform>,
WriteExpect<'s, SpatialGrid>,
);

fn run(&mut self, (entities, globals, mut spatial_grid): Self::SystemData) {
spatial_grid.reset();
for (entity, global) in (&entities, &globals).join() {
spatial_grid.insert(entity, global);
}
}
}

pub struct DebugEntityDetectionSystem;

impl<'s> System<'s> for DebugEntityDetectionSystem {
type SystemData = (
ReadStorage<'s, DetectedEntities>,
ReadStorage<'s, GlobalTransform>,
Write<'s, DebugLines>,
);

fn run(&mut self, (detected_entities, globals, mut debug_lines): Self::SystemData) {
for (detected, global) in (&detected_entities, &globals).join() {
let pos = Vector4::from(global.as_ref()[3]).xyz();
for other_entity in &detected.entities {
let other_global = globals.get(*other_entity).unwrap();
let other_pos = Vector4::from(other_global.as_ref()[3]).xyz();
debug_lines.draw_line(
[pos[0], pos[1], 0.0].into(),
[other_pos[0], other_pos[1], 0.0].into(),
[1.0, 1.0, 0.0, 1.0].into(),
);
}
}
}
}
22 changes: 7 additions & 15 deletions src/systems/health.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use amethyst::renderer::DebugLines;
use amethyst::{core::transform::ParentHierarchy, core::Transform, ecs::*};
use amethyst::{core::transform::GlobalTransform, ecs::*};
use std::f32;

use crate::components::combat::Health;
Expand All @@ -24,25 +24,17 @@ pub struct DebugHealthSystem {}

impl<'s> System<'s> for DebugHealthSystem {
type SystemData = (
Entities<'s>,
ReadExpect<'s, ParentHierarchy>,
ReadStorage<'s, Health>,
ReadStorage<'s, Transform>,
ReadStorage<'s, GlobalTransform>,
Write<'s, DebugLines>,
);

fn run(&mut self, (entities, hierarchy, healths, locals, mut debug_lines): Self::SystemData) {
for (entity, health, local) in (&entities, &healths, &locals).join() {
let pos = match hierarchy.parent(entity) {
Some(parent_entity) => {
let parent_transform = locals.get(parent_entity).unwrap();
parent_transform.clone().concat(local).translation().clone()
}
None => local.translation().clone(),
};
fn run(&mut self, (healths, globals, mut debug_lines): Self::SystemData) {
for (health, global) in (&healths, &globals).join() {
let pos: [f32; 4] = global.as_ref()[3];
debug_lines.draw_line(
[pos.x, pos.y + 0.5, 0.0].into(),
[pos.x + health.value / 100.0, pos.y + 0.5, 0.0].into(),
[pos[0], pos[1] + 0.5, 0.0].into(),
[pos[0] + health.value / 100.0, pos[1] + 0.5, 0.0].into(),
[0.0, 1.0, 0.0, 1.0].into(),
)
}
Expand Down
3 changes: 3 additions & 0 deletions src/systems/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ pub mod movement;
pub mod spawner;
pub mod swarm_behavior;
pub mod time_control;

mod experimental;
pub use experimental::*;
4 changes: 2 additions & 2 deletions src/systems/spawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ impl Distribution<CreatureTypeDistribution> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> CreatureTypeDistribution {
match rng.gen_range(0, 3) {
0 => CreatureTypeDistribution {
creature_type: "Carnivore".to_string(),
creature_type: "Herbivore".to_string(),
},
1 => CreatureTypeDistribution {
creature_type: "Herbivore".to_string(),
creature_type: "Carnivore".to_string(),
},
_ => CreatureTypeDistribution {
creature_type: "Plant".to_string(),
Expand Down

0 comments on commit cbd1ce7

Please sign in to comment.