Switch to combinator-driven NPC AI API

This commit is contained in:
Joshua Barretto 2023-01-04 11:25:39 +00:00
parent acecc62d40
commit b2f92e4a6c
5 changed files with 376 additions and 200 deletions

View File

@ -1,4 +1,4 @@
use crate::rule::npc_ai; use crate::rule::npc_ai::NpcCtx;
pub use common::rtsim::{NpcId, Profession}; pub use common::rtsim::{NpcId, Profession};
use common::{ use common::{
comp, comp,
@ -50,221 +50,295 @@ pub struct Controller {
pub goto: Option<(Vec3<f32>, f32)>, pub goto: Option<(Vec3<f32>, f32)>,
} }
#[derive(Default)] impl Controller {
pub struct TaskState { pub fn idle() -> Self { Self { goto: None } }
state: Option<Box<dyn Any + Send + Sync>>,
} }
pub const CONTINUE: ControlFlow<()> = ControlFlow::Break(()); pub trait Action<R = ()>: Any + Send + Sync {
pub const FINISH: ControlFlow<()> = ControlFlow::Continue(()); /// Returns `true` if the action should be considered the 'same' (i.e:
/// achieving the same objective) as another. In general, the AI system
/// will try to avoid switching (and therefore restarting) tasks when the
/// new task is the 'same' as the old one.
// TODO: Figure out a way to compare actions based on their 'intention': i.e:
// two pathing actions should be considered equivalent if their destination
// is the same regardless of the progress they've each made.
fn is_same(&self, other: &Self) -> bool
where
Self: Sized;
fn dyn_is_same_sized(&self, other: &dyn Action<R>) -> bool
where
Self: Sized,
{
match (other as &dyn Any).downcast_ref::<Self>() {
Some(other) => self.is_same(other),
None => false,
}
}
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool;
// Reset the action to its initial state so it can be restarted
fn reset(&mut self);
pub trait Task: PartialEq + Clone + Send + Sync + 'static { fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R>;
type State: Send + Sync;
type Ctx<'a>;
fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State; fn then<A1: Action<R1>, R1>(self, other: A1) -> Then<Self, A1, R>
where
fn run<'a>( Self: Sized,
&self, {
state: &mut Self::State, Then {
ctx: &Self::Ctx<'a>, a0: self,
controller: &mut Controller, a0_finished: false,
) -> ControlFlow<()>; a1: other,
phantom: PhantomData,
fn then<B: Task>(self, other: B) -> Then<Self, B> { Then(self, other) } }
}
fn repeat(self) -> Repeat<Self> { Repeat(self) } fn repeat<R1>(self) -> Repeat<Self, R1>
where
Self: Sized,
{
Repeat(self, PhantomData)
}
fn stop_if<F: FnMut(&mut NpcCtx) -> bool>(self, f: F) -> StopIf<Self, F>
where
Self: Sized,
{
StopIf(self, f)
}
fn map<F: FnMut(R) -> R1, R1>(self, f: F) -> Map<Self, F, R>
where
Self: Sized,
{
Map(self, f, PhantomData)
}
} }
#[derive(Clone, PartialEq)] // Now
pub struct Then<A, B>(A, B);
impl<A: Task, B> Task for Then<A, B> #[derive(Copy, Clone)]
where pub struct Now<F, A>(F, Option<A>);
B: for<'a> Task<Ctx<'a> = A::Ctx<'a>>,
impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> A + Send + Sync + 'static, A: Action<R>>
Action<R> for Now<F, A>
{ {
// TODO: Use `Either` instead // TODO: This doesn't compare?!
type Ctx<'a> = A::Ctx<'a>; fn is_same(&self, other: &Self) -> bool { true }
type State = Result<A::State, B::State>;
fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { Ok(self.0.begin(ctx)) } fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn run<'a>( fn reset(&mut self) { self.1 = None; }
&self,
state: &mut Self::State, // TODO: Reset closure state?
ctx: &Self::Ctx<'a>, fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
controller: &mut Controller, (self.1.get_or_insert_with(|| (self.0)(ctx))).tick(ctx)
) -> ControlFlow<()> {
match state {
Ok(a_state) => {
self.0.run(a_state, ctx, controller)?;
*state = Err(self.1.begin(ctx));
CONTINUE
},
Err(b_state) => self.1.run(b_state, ctx, controller),
}
} }
} }
#[derive(Clone, PartialEq)] pub fn now<F, A>(f: F) -> Now<F, A>
pub struct Repeat<A>(A); where
F: FnMut(&mut NpcCtx) -> A,
impl<A: Task> Task for Repeat<A> { {
type Ctx<'a> = A::Ctx<'a>; Now(f, None)
type State = A::State;
fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { self.0.begin(ctx) }
fn run<'a>(
&self,
state: &mut Self::State,
ctx: &Self::Ctx<'a>,
controller: &mut Controller,
) -> ControlFlow<()> {
self.0.run(state, ctx, controller)?;
*state = self.0.begin(ctx);
CONTINUE
}
} }
impl TaskState { // Just
pub fn perform<'a, T: Task>(
&mut self,
task: T,
ctx: &T::Ctx<'a>,
controller: &mut Controller,
) -> ControlFlow<()> {
type StateOf<T> = (T, <T as Task>::State);
let mut state = if let Some(state) = self.state.take().and_then(|state| { #[derive(Copy, Clone)]
state pub struct Just<F, R = ()>(F, PhantomData<R>);
.downcast::<StateOf<T>>()
.ok()
.filter(|state| state.0 == task)
}) {
state
} else {
let mut state = task.begin(ctx);
Box::new((task, state))
};
let res = state.0.run(&mut state.1, ctx, controller); impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static> Action<R>
for Just<F, R>
{
fn is_same(&self, other: &Self) -> bool { true }
self.state = if matches!(res, ControlFlow::Break(())) { fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
Some(state)
} else {
None
};
res fn reset(&mut self) {}
}
// TODO: Reset closure state?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { ControlFlow::Break((self.0)(ctx)) }
} }
pub unsafe trait Context { pub fn just<F, R: Send + Sync + 'static>(mut f: F) -> Just<F, R>
// TODO: Somehow we need to enforce this bound, I think? where
// Hence, this trait is unsafe for now. F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static,
type Ty<'a>; // where for<'a> Self::Ty<'a>: 'a; {
Just(f, PhantomData)
} }
pub struct Data<C: Context>(Arc<AtomicPtr<()>>, PhantomData<C>); // Tree
impl<C: Context> Clone for Data<C> {
fn clone(&self) -> Self { Self(self.0.clone(), PhantomData) }
}
impl<C: Context> Data<C> {
pub fn with<R>(&mut self, f: impl FnOnce(&mut C::Ty<'_>) -> R) -> R {
let ptr = self.0.swap(std::ptr::null_mut(), Ordering::Acquire);
if ptr.is_null() {
panic!("Data pointer was null, you probably tried to access data recursively")
} else {
// Safety: We have exclusive access to the pointer within this scope.
// TODO: Do we need a panic guard here?
let r = f(unsafe { &mut *(ptr as *mut C::Ty<'_>) });
self.0.store(ptr, Ordering::Release);
r
}
}
}
pub type Priority = usize; pub type Priority = usize;
pub struct TaskBox<C: Context, A = ()> { const URGENT: Priority = 0;
task: Option<( const CASUAL: Priority = 1;
TypeId,
Box<dyn Generator<Data<C>, Yield = A, Return = ()> + Unpin + Send + Sync>, pub struct Node<R>(Box<dyn Action<R>>, Priority);
Priority,
)>, pub fn urgent<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), URGENT) }
data: Data<C>, pub fn casual<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), CASUAL) }
pub struct Tree<F, R> {
next: F,
prev: Option<Node<R>>,
interrupt: bool,
} }
impl<C: Context, A> TaskBox<C, A> { impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Action<R>
pub fn new(data: Data<C>) -> Self { Self { task: None, data } } for Tree<F, R>
{
fn is_same(&self, other: &Self) -> bool { true }
#[must_use] fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
pub fn finish(&mut self, prio: Priority) -> ControlFlow<A> {
if let Some((_, task, _)) = &mut self.task.as_mut().filter(|(_, _, p)| *p <= prio) {
match Pin::new(task).resume(self.data.clone()) {
GeneratorState::Yielded(action) => ControlFlow::Break(action),
GeneratorState::Complete(_) => {
self.task = None;
ControlFlow::Continue(())
},
}
} else {
ControlFlow::Continue(())
}
}
#[must_use] fn reset(&mut self) { self.prev = None; }
pub fn perform<T: Generator<Data<C>, Yield = A, Return = ()> + Unpin + Any + Send + Sync>(
&mut self, // TODO: Reset `next` too?
prio: Priority, fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
task: T, let new = (self.next)(ctx);
) -> ControlFlow<A> {
let ty = TypeId::of::<T>(); let prev = match &mut self.prev {
if self Some(prev) if prev.1 <= new.1 && (prev.0.dyn_is_same(&*new.0) || !self.interrupt) => {
.task prev
.as_mut() },
.filter(|(ty1, _, _)| *ty1 == ty) _ => self.prev.insert(new),
.is_none()
{
self.task = Some((ty, Box::new(task), prio));
}; };
self.finish(prio) match prev.0.tick(ctx) {
} ControlFlow::Continue(()) => return ControlFlow::Continue(()),
} ControlFlow::Break(r) => {
self.prev = None;
pub struct Brain<C: Context, A = ()> { ControlFlow::Break(r)
task: Box<dyn Generator<Data<C>, Yield = A, Return = !> + Unpin + Send + Sync>,
data: Data<C>,
}
impl<C: Context, A> Brain<C, A> {
pub fn new<T: Generator<Data<C>, Yield = A, Return = !> + Unpin + Any + Send + Sync>(
task: T,
) -> Self {
Self {
task: Box::new(task),
data: Data(Arc::new(AtomicPtr::new(std::ptr::null_mut())), PhantomData),
}
}
pub fn tick(&mut self, ctx_ref: &mut C::Ty<'_>) -> A {
self.data
.0
.store(ctx_ref as *mut C::Ty<'_> as *mut (), Ordering::SeqCst);
match Pin::new(&mut self.task).resume(self.data.clone()) {
GeneratorState::Yielded(action) => {
self.data.0.store(std::ptr::null_mut(), Ordering::Release);
action
}, },
GeneratorState::Complete(ret) => match ret {},
} }
} }
} }
pub fn choose<R: 'static, F>(f: F) -> impl Action<R>
where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static,
{
Tree {
next: f,
prev: None,
interrupt: false,
}
}
pub fn watch<R: 'static, F>(f: F) -> impl Action<R>
where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static,
{
Tree {
next: f,
prev: None,
interrupt: true,
}
}
// Then
#[derive(Copy, Clone)]
pub struct Then<A0, A1, R0> {
a0: A0,
a0_finished: bool,
a1: A1,
phantom: PhantomData<R0>,
}
impl<A0: Action<R0>, A1: Action<R1>, R0: Send + Sync + 'static, R1: Send + Sync + 'static>
Action<R1> for Then<A0, A1, R0>
{
fn is_same(&self, other: &Self) -> bool {
self.a0.is_same(&other.a0) && self.a1.is_same(&other.a1)
}
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) }
fn reset(&mut self) {
self.a0.reset();
self.a0_finished = false;
self.a1.reset();
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> {
if !self.a0_finished {
match self.a0.tick(ctx) {
ControlFlow::Continue(()) => return ControlFlow::Continue(()),
ControlFlow::Break(_) => self.a0_finished = true,
}
}
self.a1.tick(ctx)
}
}
// Repeat
#[derive(Copy, Clone)]
pub struct Repeat<A, R = ()>(A, PhantomData<R>);
impl<R: Send + Sync + 'static, A: Action<R>> Action<!> for Repeat<A, R> {
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<!>) -> bool { self.dyn_is_same_sized(other) }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<!> {
match self.0.tick(ctx) {
ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(_) => {
self.0.reset();
ControlFlow::Continue(())
},
}
}
}
// StopIf
#[derive(Copy, Clone)]
pub struct StopIf<A, F>(A, F);
impl<A: Action<R>, F: FnMut(&mut NpcCtx) -> bool + Send + Sync + 'static, R> Action<Option<R>>
for StopIf<A, F>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<Option<R>>) -> bool { self.dyn_is_same_sized(other) }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<Option<R>> {
if (self.1)(ctx) {
ControlFlow::Break(None)
} else {
self.0.tick(ctx).map_break(Some)
}
}
}
// Map
#[derive(Copy, Clone)]
pub struct Map<A, F, R>(A, F, PhantomData<R>);
impl<A: Action<R>, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + 'static, R1>
Action<R1> for Map<A, F, R>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> {
self.0.tick(ctx).map_break(&mut self.1)
}
}
pub struct Brain {
pub(crate) action: Box<dyn Action<!>>,
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Npc { pub struct Npc {
// Persisted state // Persisted state
@ -292,10 +366,7 @@ pub struct Npc {
pub mode: NpcMode, pub mode: NpcMode,
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub task_state: Option<TaskState>, pub brain: Option<Brain>,
#[serde(skip_serializing, skip_deserializing)]
pub brain: Option<Brain<npc_ai::NpcData<'static>>>,
} }
impl Clone for Npc { impl Clone for Npc {
@ -310,7 +381,6 @@ impl Clone for Npc {
current_site: Default::default(), current_site: Default::default(),
goto: Default::default(), goto: Default::default(),
mode: Default::default(), mode: Default::default(),
task_state: Default::default(),
brain: Default::default(), brain: Default::default(),
} }
} }
@ -330,8 +400,7 @@ impl Npc {
current_site: None, current_site: None,
goto: None, goto: None,
mode: NpcMode::Simulated, mode: NpcMode::Simulated,
task_state: Default::default(), brain: None,
brain: Some(npc_ai::brain()),
} }
} }

View File

@ -72,12 +72,12 @@ impl Data {
.map(|e| e as f32 + 0.5) .map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
}; };
for _ in 0..1 { for _ in 0..10 {
this.npcs.create( this.npcs.create(
Npc::new(rng.gen(), rand_wpos(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng))
.with_faction(site.faction) .with_faction(site.faction)
.with_home(site_id) .with_home(site_id)
.with_profession(match 1/*rng.gen_range(0..20)*/ { .with_profession(match rng.gen_range(0..20) {
0 => Profession::Hunter, 0 => Profession::Hunter,
1 => Profession::Blacksmith, 1 => Profession::Blacksmith,
2 => Profession::Chef, 2 => Profession::Chef,

View File

@ -4,7 +4,9 @@
try_blocks, try_blocks,
generator_trait, generator_trait,
generators, generators,
trait_alias trait_alias,
trait_upcasting,
control_flow_enum
)] )]
pub mod data; pub mod data;

View File

@ -3,8 +3,8 @@ use std::{collections::VecDeque, hash::BuildHasherDefault};
use crate::{ use crate::{
data::{ data::{
npc::{ npc::{
Brain, Context, Controller, Data, Npc, NpcId, PathData, PathingMemory, Task, TaskBox, casual, choose, just, now, urgent, Action, Brain, Controller, Npc, NpcId, PathData,
TaskState, CONTINUE, FINISH, PathingMemory,
}, },
Sites, Sites,
}, },
@ -240,21 +240,27 @@ impl Rule for NpcAi {
let npc_ids = ctx.state.data().npcs.keys().collect::<Vec<_>>(); let npc_ids = ctx.state.data().npcs.keys().collect::<Vec<_>>();
for npc_id in npc_ids { for npc_id in npc_ids {
let mut task_state = ctx.state.data_mut().npcs[npc_id]
.task_state
.take()
.unwrap_or_default();
let mut brain = ctx.state.data_mut().npcs[npc_id] let mut brain = ctx.state.data_mut().npcs[npc_id]
.brain .brain
.take() .take()
.unwrap_or_else(brain); .unwrap_or_else(|| Brain {
action: Box::new(think().repeat()),
});
let (controller, task_state) = { let controller = {
let data = &*ctx.state.data(); let data = &*ctx.state.data();
let npc = &data.npcs[npc_id]; let npc = &data.npcs[npc_id];
let mut controller = Controller { goto: npc.goto }; let mut controller = Controller { goto: npc.goto };
brain.action.tick(&mut NpcCtx {
ctx: &ctx,
npc,
npc_id,
controller: &mut controller,
});
/*
let action: ControlFlow<()> = try { let action: ControlFlow<()> = try {
if matches!(npc.profession, Some(Profession::Adventurer(_))) { if matches!(npc.profession, Some(Profession::Adventurer(_))) {
if let Some(home) = npc.home { if let Some(home) = npc.home {
@ -344,12 +350,12 @@ impl Rule for NpcAi {
*/ */
} }
}; };
*/
(controller, task_state) controller
}; };
ctx.state.data_mut().npcs[npc_id].goto = controller.goto; ctx.state.data_mut().npcs[npc_id].goto = controller.goto;
ctx.state.data_mut().npcs[npc_id].task_state = Some(task_state);
ctx.state.data_mut().npcs[npc_id].brain = Some(brain); ctx.state.data_mut().npcs[npc_id].brain = Some(brain);
} }
}); });
@ -358,6 +364,99 @@ impl Rule for NpcAi {
} }
} }
pub struct NpcCtx<'a> {
ctx: &'a EventCtx<'a, NpcAi, OnTick>,
npc_id: NpcId,
npc: &'a Npc,
controller: &'a mut Controller,
}
fn idle() -> impl Action + Clone { just(|ctx| *ctx.controller = Controller::idle()) }
fn move_toward(wpos: Vec3<f32>, speed_factor: f32) -> impl Action + Clone {
const STEP_DIST: f32 = 10.0;
just(move |ctx| {
let rpos = wpos - ctx.npc.wpos;
let len = rpos.magnitude();
ctx.controller.goto = Some((
ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST),
speed_factor,
));
})
}
fn goto(wpos: Vec3<f32>, speed_factor: f32) -> impl Action + Clone {
const MIN_DIST: f32 = 1.0;
move_toward(wpos, speed_factor)
.repeat()
.stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < MIN_DIST.powi(2))
.map(|_| {})
}
// Seconds
fn timeout(ctx: &NpcCtx, time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync {
let end = ctx.ctx.event.time.0 + time;
move |ctx| ctx.ctx.event.time.0 > end
}
fn think() -> impl Action {
choose(|ctx| {
if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) {
// Choose a random site that's fairly close by
let site_wpos2d = ctx
.ctx
.state
.data()
.sites
.iter()
.filter(|(site_id, site)| {
site.faction.is_some()
&& ctx.npc.current_site.map_or(true, |cs| *site_id != cs)
&& thread_rng().gen_bool(0.25)
})
.min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32)
.map(|(site_id, _)| site_id)
.and_then(|tgt_site| {
ctx.ctx
.state
.data()
.sites
.get(tgt_site)
.map(|site| site.wpos)
});
if let Some(site_wpos2d) = site_wpos2d {
// Walk toward the site
casual(goto(
site_wpos2d.map(|e| e as f32 + 0.5).with_z(
ctx.ctx
.world
.sim()
.get_alt_approx(site_wpos2d.as_())
.unwrap_or(0.0),
),
1.0,
))
} else {
casual(idle())
}
} else if matches!(ctx.npc.profession, Some(Profession::Blacksmith)) {
casual(idle())
} else {
casual(
now(|ctx| goto(ctx.npc.wpos + Vec3::unit_x() * 10.0, 1.0))
.then(now(|ctx| goto(ctx.npc.wpos - Vec3::unit_x() * 10.0, 1.0)))
.repeat()
.stop_if(timeout(ctx, 10.0))
.then(now(|ctx| idle().repeat().stop_if(timeout(ctx, 5.0))))
.map(|_| {}),
)
}
})
}
/*
#[derive(Clone)] #[derive(Clone)]
pub struct Generate<F, T>(F, PhantomData<T>); pub struct Generate<F, T>(F, PhantomData<T>);
@ -763,3 +862,4 @@ fn walk_path(site: Id<WorldSite>, path: Path<Vec2<i32>>) -> impl IsTask {
println!("Waited."); println!("Waited.");
} }
} }
*/

View File

@ -229,11 +229,16 @@ impl<'a> AgentData<'a> {
controller.push_cancel_input(InputKind::Fly) controller.push_cancel_input(InputKind::Fly)
} }
let chase_tgt = *travel_to/*read_data.terrain
.try_find_space(travel_to.as_())
.map(|pos| pos.as_())
.unwrap_or(*travel_to)*/;
if let Some((bearing, speed)) = agent.chaser.chase( if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain, &*read_data.terrain,
self.pos.0, self.pos.0,
self.vel.0, self.vel.0,
*travel_to, chase_tgt,
TraversalConfig { TraversalConfig {
min_tgt_dist: 1.25, min_tgt_dist: 1.25,
..self.traversal_config ..self.traversal_config