Rust中有限(游戏)状态机的模式与行为的变化?

ntd*_*def 1 rust

我正在尝试在Rust中编写一个回合制游戏,而且我正在用语言碰壁(除非我不理解某些东西 - 我是语言新手).基本上,我想改变游戏中每个州有不同行为的状态.例如,我有类似的东西:

struct Game {
    state: [ Some GameState implementer ],
}

impl Game {
    fn handle(&mut self, event: Event) {
         let new_state = self.handle(event);
         self.state = new_state;
    }
}

struct ChooseAttackerPhase {
    // ...
}

struct ResolveAttacks  {
    // ...
}

impl ResolveAttacks {
    fn resolve(&self) {
        // does some stuff
    }
}

trait GameState {
    fn handle(&self, event: Event) -> [ A New GateState implementer ]
}

impl GameState for ChooseAttackerPhase {
    fn handle(&self, event: Event) -> [ A New GameState implementer ] {
        // ...
    }
}

impl GameState for ResolveAttacks {
    fn handle(&self, event: Event) -> [ A New GameState implementer ] {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我最初的计划.我想handle成为一个返回新GameState实例的纯函数.但据我所知,Rust目前无法实现.所以我尝试使用enums元组,每个元组都有各自的处理程序,最终成为一个死胡同,因为我必须匹配每个状态.

无论如何,代码不是来自我的原始项目.这只是一个例子.我的问题是:在Rust中有这样的模式,我错过了吗?我希望能够在每个状态中分离我需要做的事情的逻辑,这些逻辑对于每个状态是唯一的,并且避免编写冗长的模式匹配语句.

如果我需要更多地澄清我的问题,请告诉我.

She*_*ter 14

有限状态机(FSM)可以使用两个枚举直接建模,一个表示所有状态,另一个表示所有转换:

#[derive(Debug)]
enum Event {
    Coin,
    Push,
}

#[derive(Debug)]
enum Turnstyle {
    Locked,
    Unlocked,
}

impl Turnstyle {
    fn next(self, event: Event) -> Turnstyle {
        use Event::*;
        use Turnstyle::*;

        match self {
            Locked => {
                match event {
                    Coin => Unlocked,
                    _ => self,
                }
            },
            Unlocked => {
                match event {
                    Push => Locked,
                    _ => self,
                }
            }
        }
    }
}

fn main() {
    let t = Turnstyle::Locked;
    let t = t.next(Event::Push);
    println!("{:?}", t);
    let t = t.next(Event::Coin);
    println!("{:?}", t);
    let t = t.next(Event::Coin);
    println!("{:?}", t);
    let t = t.next(Event::Push);
    println!("{:?}", t);
}
Run Code Online (Sandbox Code Playgroud)

最大的缺点是一种方法最终会变得非常混乱所有状态/转换对.你有时可以match通过匹配对来消除一点:

match (self, event) {
    (Locked, Coin) => Unlocked,
    (Unlocked, Push) => Locked,
    (prev, _) => prev,
}
Run Code Online (Sandbox Code Playgroud)

避免编写冗长的模式匹配语句.

每个匹配臂都可以是您要为您执行的每个独特操作调用的功能.在上面,Unlocked可以用一个叫做的函数代替unlocked它做任何它需要的东西.

使用枚举[...]最终成为一个死胡同,因为我必须匹配每个州.

请注意,您可以使用它_来匹配任何模式.

枚举的一个缺点是它不能让其他人加入它.也许你想为你的游戏建立一个可扩展的系统,mods可以添加新的概念.在这种情况下,您可以使用特征:

#[derive(Debug)]
enum Event {
    Damage,
    Healing,
    Poison,
    Esuna,
}

#[derive(Debug)]
struct Player {
    state: Box<PlayerState>,
}

impl Player {
    fn handle(&mut self, event: Event) {
         let new_state = self.state.handle(event);
         self.state = new_state;
    }
}

trait PlayerState: std::fmt::Debug {
    fn handle(&self, event: Event) -> Box<PlayerState>;
}

#[derive(Debug)]
struct Healthy;
#[derive(Debug)]
struct Poisoned;

impl PlayerState for Healthy {
    fn handle(&self, event: Event) -> Box<PlayerState> {
        match event {
            Event::Poison => Box::new(Poisoned),
            _ => Box::new(Healthy),
        }
    }
}

impl PlayerState for Poisoned {
    fn handle(&self, event: Event) -> Box<PlayerState> {
        match event {
            Event::Esuna => Box::new(Healthy),
            _ => Box::new(Poisoned),
        }
    }
}

fn main() {
    let mut player = Player { state: Box::new(Healthy) };
    println!("{:?}", player);
    player.handle(Event::Damage);
    println!("{:?}", player);
    player.handle(Event::Healing);
    println!("{:?}", player);
    player.handle(Event::Poison);
    println!("{:?}", player);
    player.handle(Event::Esuna);
    println!("{:?}", player);
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以实现您想要的任何状态.

我想handle成为一个返回新GameState实例的纯函数.

您无法返回GameState实例,因为编译器需要知道每个值需要多少空间.如果你可以返回一个结构,它在一次调用中占用4个字节,或者从另一个调用中占用8个字节,编译器就不会知道你实际需要多少空间.

您必须做出的权衡是始终返回一个新分配的特征对象.需要进行此分配,以便为PlayerState可能出现的每种可能变体提供同质大小.

在将来,可能会支持说函数返回特征(fn things() -> impl Iterator例如).这基本上是隐藏的事实,有与已知大小的程序员不/不能写一个值.如果我理解正确的话,它会不会在这种情况下帮助,因为规模的不确定性不会在编译时确定.

极少数情况下,您的状态没有任何实际状态,您可以创建每个状态的共享,不可变的全局实例:

trait PlayerState: std::fmt::Debug {
    fn handle(&self, event: Event) -> &'static PlayerState;
}

static HEALTHY: Healthy = Healthy;
static POISONED: Poisoned = Poisoned;

impl PlayerState for Healthy {
    fn handle(&self, event: Event) -> &'static PlayerState {
        match event {
            Event::Poison => &POISONED,
            _ => &HEALTHY,
        }
    }
}

impl PlayerState for Poisoned {
    fn handle(&self, event: Event) -> &'static PlayerState {
        match event {
            Event::Esuna => &HEALTHY,
            _ => &POISONED,
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这将避免分配的开销(无论可能是什么).我不会尝试这个,直到你知道没有状态,并且在分配中花费了很多时间.