Skip to content

Commit

Permalink
Add a basic implementation of the Topplegrass creature
Browse files Browse the repository at this point in the history
Refer to issue amethyst#61
Topplegrass is explained here: https://community.amethyst.rs/t/evoli-creature-designs/814/10

Any entity with the Movement component intelligently avoids obstacles. However, not all entities that can move can steer; for example, Topplegrass is moved only by the wind. Therefore, add a new component tag AvoidObstaclesTag to all entities that are supposed to steer.

Add new resource Wind. Implement debug keyboard input action to change wind direction. Load initial wind direction from a config file. In order to load from file, pass the assets directory path to the loading state.

Add system that spawns new Topplegrass entities.

Add system that deletes Topplegrass entities when they leave the world bounds.
  • Loading branch information
Jazarro committed Aug 31, 2019
1 parent 4425158 commit 053fd37
Show file tree
Hide file tree
Showing 19 changed files with 324 additions and 5 deletions.
3 changes: 3 additions & 0 deletions resources/assets/topplegrass.bin
Git LFS file not shown
3 changes: 3 additions & 0 deletions resources/assets/topplegrass.gltf
Git LFS file not shown
3 changes: 3 additions & 0 deletions resources/input.ron
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@
"CameraMoveBackward": [
[Key(LShift), Key(Down)]
],
"ChangeWindDirection": [
[Key(W)]
],
},
)
1 change: 1 addition & 0 deletions resources/prefabs/creatures/carnivore.ron
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Prefab (
),
),
intelligence_tag: (),
avoid_obstacles_tag: (),
perception: (
range: 3.0,
),
Expand Down
1 change: 1 addition & 0 deletions resources/prefabs/creatures/herbivore.ron
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Prefab (
),
),
intelligence_tag: (),
avoid_obstacles_tag: (),
perception: (
range: 2.5,
),
Expand Down
14 changes: 14 additions & 0 deletions resources/prefabs/creatures/topplegrass.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#![enable(implicit_some)]
Prefab (
entities: [
(
data: (
name: (
name: "Topplegrass"
),
gltf: File("assets/Topplegrass.gltf", ()),
despawn_when_out_of_bounds_tag: (),
),
),
],
)
3 changes: 3 additions & 0 deletions resources/wind.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(
wind: [10.0, 0.0],
)
22 changes: 22 additions & 0 deletions src/components/creatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ impl Component for RicochetTag {
type Storage = NullStorage<Self>;
}

/// Entities tagged with this Component (and of course a Transform and Movement) will actively
/// avoid obstacles by steering away from them.
/// The world bounds currently (v0.2.0) are the only obstacles.
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PrefabData)]
#[prefab(Component)]
pub struct AvoidObstaclesTag;

impl Component for AvoidObstaclesTag {
type Storage = NullStorage<Self>;
}

/// Entities tagged with this Component will despawn as soon as their position is outside the world bounds.
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PrefabData)]
#[prefab(Component)]
pub struct DespawnWhenOutOfBoundsTag;

impl Component for DespawnWhenOutOfBoundsTag {
type Storage = NullStorage<Self>;
}

#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PrefabData)]
#[prefab(Component)]
pub struct IntelligenceTag;
Expand Down Expand Up @@ -96,4 +116,6 @@ pub struct CreaturePrefabData {
intelligence_tag: Option<IntelligenceTag>,
perception: Option<Perception>,
ricochet_tag: Option<RicochetTag>,
avoid_obstacles_tag: Option<AvoidObstaclesTag>,
despawn_when_out_of_bounds_tag: Option<DespawnWhenOutOfBoundsTag>,
}
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ fn main() -> amethyst::Result<()> {

// Set up the core application.
let mut game: Application<GameData> =
CoreApplication::build(resources, LoadingState::default())?
CoreApplication::build(resources.clone(), LoadingState::new(resources))?
.with_frame_limit(FrameRateLimitStrategy::Sleep, 60)
.build(game_data)?;
game.run();
Expand Down
1 change: 1 addition & 0 deletions src/resources/experimental/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod spatial_grid;
pub mod wind;
25 changes: 25 additions & 0 deletions src/resources/experimental/wind.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use amethyst::core::math::Vector2;
use serde::{Deserialize, Serialize};

/// Keeps track of the wind conditions in the world.
/// Currently, wind is represented by a 2D vector.
#[derive(Deserialize, Serialize)]
#[serde(default)]
#[serde(deny_unknown_fields)]
pub struct Wind {
pub wind: Vector2<f32>,
}

impl Wind {
pub fn new(x: f32, y: f32) -> Wind {
Wind {
wind: Vector2::new(x, y),
}
}
}

impl Default for Wind {
fn default() -> Self {
Wind::new(0.0, 0.0)
}
}
15 changes: 15 additions & 0 deletions src/states/loading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{
resources::{
audio::initialise_audio,
prefabs::{initialize_prefabs, update_prefabs},
wind::*,
world_bounds::WorldBounds,
},
states::{main_game::MainGameState, menu::MenuState},
Expand All @@ -18,12 +19,23 @@ use amethyst::{
const SKIP_MENU_ARG: &str = "no_menu";

pub struct LoadingState {
config_path: String,
prefab_loading_progress: Option<ProgressCounter>,
}

impl Default for LoadingState {
fn default() -> Self {
LoadingState {
config_path: "".to_string(),
prefab_loading_progress: None,
}
}
}

impl LoadingState {
pub fn new(config_path: String) -> Self {
LoadingState {
config_path: config_path,
prefab_loading_progress: None,
}
}
Expand All @@ -40,6 +52,9 @@ impl SimpleState for LoadingState {
data.world.add_resource(DebugLines::new());
data.world
.add_resource(WorldBounds::new(-10.0, 10.0, -10.0, 10.0));
let wind_config_path = self.config_path.clone() + "/wind.ron";
let wind_config = Wind::load(wind_config_path);
data.world.add_resource(wind_config);
}

fn update(&mut self, data: &mut StateData<GameData>) -> SimpleTrans {
Expand Down
17 changes: 17 additions & 0 deletions src/states/main_game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,21 @@ impl MainGameState {
"swarm_spawn",
&[],
)
.with(
topplegrass::TopplegrassSpawnSystem::default(),
"topplegrass_spawn_system",
&[],
)
.with(
out_of_bounds::OutOfBoundsDespawnSystem::default(),
"out_of_bounds_despawn_system",
&[],
)
.with(
wind_control::DebugWindControlSystem::default(),
"wind_control_system",
&[],
)
.with(
swarm_behavior::SwarmBehaviorSystem::default(),
"swarm_behavior",
Expand Down Expand Up @@ -336,6 +351,8 @@ impl SimpleState for MainGameState {
.world
.write_resource::<EventChannel<spawner::CreatureSpawnEvent>>();
// TODO unfortunate naming here; plants are not creatures...OrganismSpawnEvent or just SpawnEvent?
// I would go for something more generic than OrganismSpawnEvent; for example,
// Topplegrass isn't really one organism, but more of a set of organisms, both dead and alive.
spawn_events.single_write(spawner::CreatureSpawnEvent {
creature_type: "Plant".to_string(),
entity: plant_entity,
Expand Down
9 changes: 6 additions & 3 deletions src/systems/behaviors/obstacle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use amethyst::{

use std::cmp::Ordering;

use crate::components::creatures::Movement;
use crate::components::creatures::{AvoidObstaclesTag, Movement};
use crate::resources::world_bounds::WorldBounds;
use crate::systems::behaviors::decision::Closest;

Expand Down Expand Up @@ -47,19 +47,22 @@ impl<'s> System<'s> for ClosestObstacleSystem {
ReadStorage<'s, Transform>,
ReadStorage<'s, Movement>,
ReadExpect<'s, WorldBounds>,
ReadStorage<'s, AvoidObstaclesTag>,
WriteStorage<'s, Closest<Obstacle>>,
);

fn run(
&mut self,
(entities, transforms, movements, world_bounds, mut closest_obstacle): Self::SystemData,
(entities, transforms, movements, world_bounds, avoid_obstacles, mut closest_obstacle): Self::SystemData,
) {
// Right now the only obstacles are the world bound walls, so it's
// safe to clear this out.
closest_obstacle.clear();

let threshold = 3.0f32.powi(2);
for (entity, transform, _) in (&entities, &transforms, &movements).join() {
for (entity, transform, _, _) in
(&entities, &transforms, &avoid_obstacles, &movements).join()
{
// Find the closest wall to this entity
let wall_dir = closest_wall(&transform.translation(), &world_bounds);
if wall_dir.magnitude_squared() < threshold {
Expand Down
3 changes: 3 additions & 0 deletions src/systems/experimental/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
pub mod out_of_bounds;
pub mod perception;
pub mod topplegrass;
pub mod wind_control;
31 changes: 31 additions & 0 deletions src/systems/experimental/out_of_bounds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use crate::resources::world_bounds::WorldBounds;
use amethyst::{core::transform::components::Transform, ecs::*};

use crate::components::creatures::DespawnWhenOutOfBoundsTag;

/// Deletes any entity tagged with DespawnWhenOutOfBoundsTag if they are detected to be outside
/// the world bounds.
#[derive(Default)]
pub struct OutOfBoundsDespawnSystem;

impl<'s> System<'s> for OutOfBoundsDespawnSystem {
type SystemData = (
Entities<'s>,
ReadStorage<'s, Transform>,
ReadStorage<'s, DespawnWhenOutOfBoundsTag>,
ReadExpect<'s, WorldBounds>,
);

fn run(&mut self, (entities, locals, tags, bounds): Self::SystemData) {
for (entity, local, _) in (&*entities, &locals, &tags).join() {
let pos = local.translation();
if pos.x > bounds.right
|| pos.x < bounds.left
|| pos.y > bounds.top
|| pos.y < bounds.bottom
{
let _ = entities.delete(entity);
}
}
}
}
107 changes: 107 additions & 0 deletions src/systems/experimental/topplegrass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use crate::resources::world_bounds::WorldBounds;
use amethyst::{
core::{
math::{Vector2, Vector3},
timing::Time,
transform::components::Transform,
},
ecs::*,
shrev::EventChannel,
};

use rand::{thread_rng, Rng};
use std::f32;

use crate::{
components::creatures::Movement, resources::wind::Wind, systems::spawner::CreatureSpawnEvent,
};

/// A new topplegrass entity is spawned periodically, SPAWN_INTERVAL is the period in seconds.
const SPAWN_INTERVAL: f32 = 0.5;
/// The standard scaling to apply to the entity.
const TOPPLEGRASS_BASE_SCALE: f32 = 0.002;
/// The maximum movement speed of Topplegrass.
const MAX_MOVEMENT_SPEED: f32 = 1.75;

/// Periodically spawns a Topplegrass entity.
#[derive(Default)]
pub struct TopplegrassSpawnSystem {
secs_to_next_spawn: f32,
}

/// Periodically schedules a Topplegrass entity to be spawned in through a CreatureSpawnEvent.
impl<'s> System<'s> for TopplegrassSpawnSystem {
type SystemData = (
Entities<'s>,
Read<'s, LazyUpdate>,
Write<'s, EventChannel<CreatureSpawnEvent>>,
Read<'s, Time>,
Read<'s, WorldBounds>,
Read<'s, Wind>,
);

fn run(
&mut self,
(entities, lazy_update, mut spawn_events, time, world_bounds, wind): Self::SystemData,
) {
if self.ready_to_spawn(time.delta_seconds()) {
let mut transform = Transform::default();
transform.set_scale(Vector3::new(
TOPPLEGRASS_BASE_SCALE,
TOPPLEGRASS_BASE_SCALE,
TOPPLEGRASS_BASE_SCALE,
));
transform.append_translation(Self::gen_spawn_location(&wind, &world_bounds));
let movement = Movement {
velocity: Vector3::new(wind.wind.x, wind.wind.y, 0.0),
max_movement_speed: MAX_MOVEMENT_SPEED,
};
let entity = lazy_update
.create_entity(&entities)
.with(transform)
.with(movement)
.build();
spawn_events.single_write(CreatureSpawnEvent {
creature_type: "Topplegrass".to_string(),
entity,
});
}
}
}

impl TopplegrassSpawnSystem {
/// Checks the time elapsed since the last spawn. If the system is ready to spawn another
/// entity, the timer will be reset and this function will return true.
fn ready_to_spawn(&mut self, delta_seconds: f32) -> bool {
self.secs_to_next_spawn -= delta_seconds;
if self.secs_to_next_spawn.is_sign_negative() {
self.secs_to_next_spawn = SPAWN_INTERVAL;
true
} else {
false
}
}

/// Returns a Vector3<f32> representing the position in which to spawn the next entity.
/// Entities will be spawned at a random point on one of the four world borders; specifically,
/// the one that the wind direction is facing away from. In other words: upwind from the
/// center of the world.
fn gen_spawn_location(wind: &Wind, bounds: &WorldBounds) -> Vector3<f32> {
let mut rng = thread_rng();
if Self::wind_towards_direction(wind.wind, Vector2::new(1.0, 0.0)) {
Vector3::new(bounds.left, rng.gen_range(bounds.bottom, bounds.top), 0.5)
} else if Self::wind_towards_direction(wind.wind, Vector2::new(0.0, 1.0)) {
Vector3::new(rng.gen_range(bounds.left, bounds.right), bounds.bottom, 0.5)
} else if Self::wind_towards_direction(wind.wind, Vector2::new(-1.0, 0.0)) {
Vector3::new(bounds.right, rng.gen_range(bounds.bottom, bounds.top), 0.5)
} else {
Vector3::new(rng.gen_range(bounds.left, bounds.right), bounds.top, 0.5)
}
}

/// Returns true if and only if the given wind vector is roughly in line with the given
/// cardinal_direction vector, within a margin of a 1/4 PI RAD.
fn wind_towards_direction(wind: Vector2<f32>, cardinal_direction: Vector2<f32>) -> bool {
wind.angle(&cardinal_direction).abs() < f32::consts::FRAC_PI_4
}
}
Loading

0 comments on commit 053fd37

Please sign in to comment.