Added basic rtsim NPC simulation, rtsim controller support

This commit is contained in:
Joshua Barretto 2022-08-11 21:54:35 +01:00
parent 8ff438bb5b
commit 558dd99fd3
13 changed files with 139 additions and 229 deletions

View File

@ -57,7 +57,7 @@ pub struct RtSimController {
/// When this field is `Some(..)`, the agent should attempt to make progress
/// toward the given location, accounting for obstacles and other
/// high-priority situations like being attacked.
pub travel_to: Option<(Vec3<f32>, String)>,
pub travel_to: Option<Vec3<f32>>,
/// Proportion of full speed to move
pub speed_factor: f32,
/// Events
@ -75,12 +75,10 @@ impl Default for RtSimController {
}
impl RtSimController {
pub fn reset(&mut self) { *self = Self::default(); }
pub fn with_destination(pos: Vec3<f32>) -> Self {
Self {
travel_to: Some((pos, format!("{:0.1?}", pos))),
speed_factor: 0.25,
travel_to: Some(pos),
speed_factor: 0.5,
events: Vec::new(),
}
}

View File

@ -28,6 +28,8 @@ pub struct Data {
pub nature: Nature,
pub npcs: Npcs,
pub sites: Sites,
pub time: f64,
}
pub type ReadError = rmp_serde::decode::Error;

View File

@ -2,50 +2,17 @@ use hashbrown::HashMap;
use serde::{Serialize, Deserialize};
use slotmap::HopSlotMap;
use vek::*;
use rand::prelude::*;
use std::ops::{Deref, DerefMut};
use common::{
uid::Uid,
store::Id,
rtsim::SiteId,
rtsim::{SiteId, RtSimController},
comp,
};
use world::util::RandomPerm;
pub use common::rtsim::NpcId;
#[derive(Clone, Serialize, Deserialize)]
pub struct Npc {
// Persisted state
/// Represents the location of the NPC.
pub loc: NpcLoc,
// Unpersisted state
/// The position of the NPC in the world. Note that this is derived from [`Npc::loc`] and cannot be updated manually
#[serde(skip_serializing, skip_deserializing)]
wpos: Vec3<f32>,
/// Whether the NPC is in simulated or loaded mode (when rtsim is run on the server, loaded corresponds to being
/// within a loaded chunk). When in loaded mode, the interactions of the NPC should not be simulated but should
/// instead be derived from the game.
#[serde(skip_serializing, skip_deserializing)]
pub mode: NpcMode,
}
impl Npc {
pub fn new(loc: NpcLoc) -> Self {
Self {
loc,
wpos: Vec3::zero(),
mode: NpcMode::Simulated,
}
}
pub fn wpos(&self) -> Vec3<f32> { self.wpos }
/// You almost certainly *DO NOT* want to use this method.
///
/// Update the NPC's wpos as a result of routine NPC simulation derived from its location.
pub(crate) fn tick_wpos(&mut self, wpos: Vec3<f32>) { self.wpos = wpos; }
}
#[derive(Copy, Clone, Default)]
pub enum NpcMode {
/// The NPC is unloaded and is being simulated via rtsim.
@ -56,14 +23,46 @@ pub enum NpcMode {
}
#[derive(Clone, Serialize, Deserialize)]
pub enum NpcLoc {
Wild { wpos: Vec3<f32> },
Site { site: SiteId, wpos: Vec3<f32> },
Travelling {
a: SiteId,
b: SiteId,
frac: f32,
},
pub struct Npc {
// Persisted state
/// Represents the location of the NPC.
pub seed: u32,
pub wpos: Vec3<f32>,
// Unpersisted state
/// (wpos, speed_factor)
#[serde(skip_serializing, skip_deserializing)]
pub target: Option<(Vec3<f32>, f32)>,
/// Whether the NPC is in simulated or loaded mode (when rtsim is run on the server, loaded corresponds to being
/// within a loaded chunk). When in loaded mode, the interactions of the NPC should not be simulated but should
/// instead be derived from the game.
#[serde(skip_serializing, skip_deserializing)]
pub mode: NpcMode,
}
impl Npc {
const PERM_SPECIES: u32 = 0;
const PERM_BODY: u32 = 1;
pub fn new(seed: u32, wpos: Vec3<f32>) -> Self {
Self {
seed,
wpos,
target: None,
mode: NpcMode::Simulated,
}
}
pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed + perm) }
pub fn get_body(&self) -> comp::Body {
let species = *(&comp::humanoid::ALL_SPECIES)
.choose(&mut self.rng(Self::PERM_SPECIES))
.unwrap();
comp::humanoid::Body::random_with(&mut self.rng(Self::PERM_BODY), &species).into()
}
}
#[derive(Clone, Serialize, Deserialize)]

View File

@ -17,5 +17,8 @@ pub struct OnSetup;
impl Event for OnSetup {}
#[derive(Clone)]
pub struct OnTick { pub dt: f32 }
pub struct OnTick {
pub dt: f32,
pub time: f64,
}
impl Event for OnTick {}

View File

@ -1,7 +1,7 @@
pub mod site;
use crate::data::{
npc::{Npcs, Npc, NpcLoc},
npc::{Npcs, Npc},
site::{Sites, Site},
Data,
Nature,
@ -25,6 +25,8 @@ impl Data {
nature: Nature::generate(world),
npcs: Npcs { npcs: Default::default() },
sites: Sites { sites: Default::default() },
time: 0.0,
};
// Register sites with rtsim
@ -39,10 +41,12 @@ impl Data {
// Spawn some test entities at the sites
for (site_id, site) in this.sites.iter() {
let wpos = site.wpos.map(|e| e as f32)
.with_z(world.sim().get_alt_approx(site.wpos).unwrap_or(0.0));
this.npcs.create(Npc::new(NpcLoc::Site { site: site_id, wpos }));
println!("Spawned rtsim NPC at {:?}", wpos);
for _ in 0..10 {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
let wpos = wpos2d.map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0));
this.npcs.create(Npc::new(rng.gen(), wpos));
}
}
this

View File

@ -49,8 +49,8 @@ impl RtState {
fn start_default_rules(&mut self) {
info!("Starting default rtsim rules...");
self.start_rule::<rule::simulate_npcs::SimulateNpcs>();
self.start_rule::<rule::setup::Setup>();
self.start_rule::<rule::simulate_npcs::SimulateNpcs>();
}
pub fn start_rule<R: Rule>(&mut self) {
@ -110,6 +110,8 @@ impl RtState {
}
pub fn tick(&mut self, world: &World, index: IndexRef, dt: f32) {
self.emit(OnTick { dt }, world, index);
self.data_mut().time += dt as f64;
let event = OnTick { dt, time: self.data().time };
self.emit(event, world, index);
}
}

View File

@ -1,6 +1,7 @@
use tracing::info;
use vek::*;
use crate::{
data::npc::NpcLoc,
data::npc::NpcMode,
event::OnTick,
RtState, Rule, RuleError,
};
@ -11,12 +12,33 @@ impl Rule for SimulateNpcs {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnTick>(|ctx| {
for (_, npc) in ctx.state.data_mut().npcs.iter_mut() {
npc.tick_wpos(match npc.loc {
NpcLoc::Wild { wpos } => wpos,
NpcLoc::Site { site, wpos } => wpos,
NpcLoc::Travelling { a, b, frac } => todo!(),
});
for npc in ctx.state
.data_mut()
.npcs
.values_mut()
.filter(|npc| matches!(npc.mode, NpcMode::Simulated))
{
let body = npc.get_body();
if let Some((target, speed_factor)) = npc.target {
npc.wpos += Vec3::from(
(target.xy() - npc.wpos.xy())
.try_normalized()
.unwrap_or_else(Vec2::zero)
* body.max_speed_approx()
* speed_factor,
) * ctx.event.dt;
}
}
// Do some thinking. TODO: Not here!
for npc in ctx.state
.data_mut()
.npcs
.values_mut()
{
// TODO: Not this
npc.target = Some((npc.wpos + Vec3::new(ctx.event.time.sin() as f32 * 16.0, ctx.event.time.cos() as f32 * 16.0, 0.0), 1.0));
}
});

View File

@ -212,7 +212,7 @@ impl<'a> AgentData<'a> {
}
agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0;
if let Some((travel_to, _destination)) = &agent.rtsim_controller.travel_to {
if let Some(travel_to) = &agent.rtsim_controller.travel_to {
// If it has an rtsim destination and can fly, then it should.
// If it is flying and bumps something above it, then it should move down.
if self.traversal_config.can_fly

View File

@ -54,6 +54,7 @@ pub struct AgentData<'a> {
pub stance: Option<&'a Stance>,
pub cached_spatial_grid: &'a common::CachedSpatialGrid,
pub msm: &'a MaterialStatManifest,
pub rtsim_entity: Option<&'a RtSimEntity>,
}
pub struct TargetData<'a> {
@ -236,7 +237,7 @@ pub struct ReadData<'a> {
pub light_emitter: ReadStorage<'a, LightEmitter>,
#[cfg(feature = "worldgen")]
pub world: ReadExpect<'a, Arc<world::World>>,
// pub rtsim_entities: ReadStorage<'a, RtSimEntity>,
pub rtsim_entities: ReadStorage<'a, RtSimEntity>,
pub buffs: ReadStorage<'a, Buffs>,
pub combos: ReadStorage<'a, Combo>,
pub active_abilities: ReadStorage<'a, ActiveAbilities>,

View File

@ -8,7 +8,7 @@ use common::{
generation::{BodyBuilder, EntityConfig, EntityInfo},
resources::{DeltaTime, Time},
slowjob::SlowJobPool,
rtsim::RtSimEntity,
rtsim::{RtSimEntity, RtSimController},
};
use common_ecs::{Job, Origin, Phase, System};
use rtsim2::data::npc::NpcMode;
@ -26,6 +26,9 @@ impl<'a> System<'a> for Sys {
ReadExpect<'a, Arc<world::World>>,
ReadExpect<'a, world::IndexOwned>,
ReadExpect<'a, SlowJobPool>,
ReadStorage<'a, comp::Pos>,
ReadStorage<'a, RtSimEntity>,
WriteStorage<'a, comp::Agent>,
);
const NAME: &'static str = "rtsim::tick";
@ -34,7 +37,7 @@ impl<'a> System<'a> for Sys {
fn run(
_job: &mut Job<Self>,
(dt, time, mut server_event_bus, mut rtsim, world, index, slow_jobs): Self::SystemData,
(dt, time, mut server_event_bus, mut rtsim, world, index, slow_jobs, positions, rtsim_entities, mut agents): Self::SystemData,
) {
let mut emitter = server_event_bus.emitter();
let rtsim = &mut *rtsim;
@ -47,7 +50,7 @@ impl<'a> System<'a> for Sys {
let chunk_states = rtsim.state.resource::<ChunkStates>();
for (npc_id, npc) in rtsim.state.data_mut().npcs.iter_mut() {
let chunk = npc.wpos()
let chunk = npc.wpos
.xy()
.map2(TerrainChunk::RECT_SIZE, |e, sz| (e as i32).div_euclid(sz as i32));
@ -55,18 +58,18 @@ impl<'a> System<'a> for Sys {
if matches!(npc.mode, NpcMode::Simulated) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) {
npc.mode = NpcMode::Loaded;
let body = comp::Body::Object(comp::object::Body::Scarecrow);
let body = npc.get_body();
emitter.emit(ServerEvent::CreateNpc {
pos: comp::Pos(npc.wpos()),
pos: comp::Pos(npc.wpos),
stats: comp::Stats::new("Rtsim NPC".to_string()),
skill_set: comp::SkillSet::default(),
health: None,
poise: comp::Poise::new(body),
inventory: comp::Inventory::with_empty(),
body,
agent: None,
agent: Some(comp::Agent::from_body(&body)),
alignment: comp::Alignment::Wild,
scale: comp::Scale(10.0),
scale: comp::Scale(1.0),
anchor: None,
loot: Default::default(),
rtsim_entity: Some(RtSimEntity(npc_id)),
@ -75,147 +78,24 @@ impl<'a> System<'a> for Sys {
}
}
// rtsim.tick += 1;
// Update unloaded rtsim entities, in groups at a time
/*
const TICK_STAGGER: usize = 30;
let entities_per_iteration = rtsim.entities.len() / TICK_STAGGER;
let mut to_reify = Vec::new();
for (id, entity) in rtsim
.entities
.iter_mut()
.skip((rtsim.tick as usize % TICK_STAGGER) * entities_per_iteration)
.take(entities_per_iteration)
.filter(|(_, e)| !e.is_loaded)
{
if rtsim
.chunk_states
.get(entity.pos.xy())
.copied()
.unwrap_or(false)
{
to_reify.push(id);
} else {
// Simulate behaviour
if let Some(travel_to) = &entity.controller.travel_to {
// Move towards target at approximate character speed
entity.pos += Vec3::from(
(travel_to.0.xy() - entity.pos.xy())
.try_normalized()
.unwrap_or_else(Vec2::zero)
* entity.get_body().max_speed_approx()
* entity.controller.speed_factor,
) * dt;
}
if let Some(alt) = world
.sim()
.get_alt_approx(entity.pos.xy().map(|e| e.floor() as i32))
{
entity.pos.z = alt;
}
}
// entity.tick(&time, &terrain, &world, &index.as_index_ref());
}
*/
// Tick entity AI each time if it's loaded
// for (_, entity) in rtsim.entities.iter_mut().filter(|(_, e)|
// e.is_loaded) { entity.last_time_ticked = time.0;
// entity.tick(&time, &terrain, &world, &index.as_index_ref());
// }
/*
let mut server_emitter = server_event_bus.emitter();
for id in to_reify {
rtsim.reify_entity(id);
let entity = &rtsim.entities[id];
let rtsim_entity = Some(RtSimEntity(id));
let body = entity.get_body();
let spawn_pos = terrain
.find_space(entity.pos.map(|e| e.floor() as i32))
.map(|e| e as f32)
+ Vec3::new(0.5, 0.5, body.flying_height());
let pos = comp::Pos(spawn_pos);
let event = if let comp::Body::Ship(ship) = body {
ServerEvent::CreateShip {
pos,
ship,
mountable: false,
agent: Some(comp::Agent::from_body(&body)),
rtsim_entity,
}
} else {
let entity_config_path = entity.get_entity_config();
let mut loadout_rng = entity.loadout_rng();
let ad_hoc_loadout = entity.get_adhoc_loadout();
// Body is rewritten so that body parameters
// are consistent between reifications
let entity_config = EntityConfig::from_asset_expect_owned(entity_config_path)
.with_body(BodyBuilder::Exact(body));
let mut entity_info = EntityInfo::at(pos.0)
.with_entity_config(entity_config, Some(entity_config_path), &mut loadout_rng)
.with_lazy_loadout(ad_hoc_loadout);
// Merchants can be traded with
if let Some(economy) = entity.get_trade_info(&world, &index) {
entity_info = entity_info
.with_agent_mark(comp::agent::Mark::Merchant)
.with_economy(&economy);
}
match NpcData::from_entity_info(entity_info) {
NpcData::Data {
pos,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
loot,
} => ServerEvent::CreateNpc {
pos,
stats,
skill_set,
health,
poise,
inventory,
agent,
body,
alignment,
scale,
anchor: None,
loot,
rtsim_entity,
projectile: None,
},
// EntityConfig can't represent Waypoints at all
// as of now, and if someone will try to spawn
// rtsim waypoint it is definitely error.
NpcData::Waypoint(_) => unimplemented!(),
}
};
server_emitter.emit(event);
}
// Update rtsim with real entity data
for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, &mut agents).join() {
// Synchronise rtsim NPC with entity data
for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, (&mut agents).maybe()).join() {
rtsim
.entities
.state
.data_mut()
.npcs
.get_mut(rtsim_entity.0)
.filter(|e| e.is_loaded)
.map(|entity| {
entity.pos = pos.0;
agent.rtsim_controller = entity.controller.clone();
.filter(|npc| matches!(npc.mode, NpcMode::Loaded))
.map(|npc| {
// Update rtsim NPC state
npc.wpos = pos.0;
// Update entity state
if let Some(agent) = agent {
agent.rtsim_controller.travel_to = npc.target.map(|(wpos, _)| wpos);
agent.rtsim_controller.speed_factor = npc.target.map_or(1.0, |(_, sf)| sf);
}
});
}
*/
}
}

View File

@ -206,6 +206,7 @@ impl<'a> System<'a> for Sys {
msm: &read_data.msm,
poise: read_data.poises.get(entity),
stance: read_data.stances.get(entity),
rtsim_entity: read_data.rtsim_entities.get(entity),
};
///////////////////////////////////////////////////////////

View File

@ -1,4 +1,4 @@
use crate::rtsim::Entity as RtSimEntity;
use common::rtsim::RtSimEntity;
use common::{
comp::{
agent::{
@ -40,8 +40,6 @@ mod interaction;
pub struct BehaviorData<'a, 'b, 'c> {
pub agent: &'a mut Agent,
pub agent_data: AgentData<'a>,
// TODO: Move rtsim back into AgentData after rtsim2 when it has a separate crate
// pub rtsim_entity: Option<&'a RtSimEntity>,
pub read_data: &'a ReadData<'a>,
pub event_emitter: &'a mut Emitter<'c, ServerEvent>,
pub controller: &'a mut Controller,
@ -643,7 +641,6 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
let BehaviorData {
agent,
agent_data,
// rtsim_entity,
read_data,
event_emitter,
controller,
@ -750,7 +747,7 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
controller,
read_data,
event_emitter,
will_ambush(/* *rtsim_entity */None, agent_data),
will_ambush(agent_data.rtsim_entity, agent_data),
);
}
@ -768,9 +765,9 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
read_data,
event_emitter,
rng,
remembers_fight_with(/* *rtsim_entity */None, read_data, target),
remembers_fight_with(agent_data.rtsim_entity, read_data, target),
);
remember_fight(/* *rtsim_entity */ None, read_data, agent, target);
remember_fight(agent_data.rtsim_entity, read_data, agent, target);
}
}
}
@ -780,10 +777,11 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
fn will_ambush(rtsim_entity: Option<&RtSimEntity>, agent_data: &AgentData) -> bool {
// TODO: implement for rtsim2
agent_data
.health
.map_or(false, |h| h.current() / h.maximum() > 0.7)
&& rtsim_entity.map_or(false, |re| re.brain.personality.will_ambush)
// agent_data
// .health
// .map_or(false, |h| h.current() / h.maximum() > 0.7)
// && rtsim_entity.map_or(false, |re| re.brain.personality.will_ambush)
false
}
fn remembers_fight_with(
@ -794,11 +792,12 @@ fn remembers_fight_with(
// TODO: implement for rtsim2
let name = || read_data.stats.get(other).map(|stats| stats.name.clone());
rtsim_entity.map_or(false, |rtsim_entity| {
name().map_or(false, |name| {
rtsim_entity.brain.remembers_fight_with_character(&name)
})
})
// rtsim_entity.map_or(false, |rtsim_entity| {
// name().map_or(false, |name| {
// rtsim_entity.brain.remembers_fight_with_character(&name)
// })
// })
false
}
/// Remember target.

View File

@ -16,7 +16,6 @@ use rand::{thread_rng, Rng};
use specs::saveload::Marker;
use crate::{
rtsim::entity::{PersonalityTrait, RtSimEntityKind},
sys::agent::util::get_entity_by_id,
};