首页 文章

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

提问于
浏览
1

我正在尝试在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 ] {
        // ...
    }
}

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

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

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

2 回答

  • 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);
    }
    

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

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

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

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

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

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

    枚举的一个缺点是它不能让其他人加入它 . 也许你想为你的游戏 Build 一个可扩展的系统,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);
    }
    

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

    我希望handle是一个返回新GameState实例的纯函数 .

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

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

    在将来,可能会支持说函数返回一个特征(例如 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,
            }
        }
    }
    

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

  • 1

    我'm experimenting with encoding the FSM into the type model. This requires each state and each event to have it'的类型,但我想's just bytes underneath and the explicit types allow me to break the transitions apart. Here' s playground with a tourniquet的例子 .

    我们从最简单的假设开始 . 机器由它的状态和转换表示 . 事件将机器一步转换到新状态,消耗旧状态 . 这允许机器以不可变状态和事件结构编码 . 各州实现此通用 Machine 特征来添加转换:

    pub trait Machine<TEvent> {
        type State;
        fn step(self, event: TEvent) -> Self::State;
    }
    

    这就是这种模式的所有框架 . 其余的是应用和实施 . 您无法进行未定义的转换,也不会出现不可预测的状态 . 它看起来很可读 . 例如:

    enum State {
        Go(Open),
        Wait(Locked),
    }
    
    struct Locked {
        price: u8,
        credit: u8,
    }
    
    struct Open {
        price: u8,
        credit: u8,
    }
    
    struct Coin {
        value: u8,
    }
    
    impl Machine<Coin> for Locked {
        type State = State;
        fn step(self, coin: Coin) -> Self::State {
            let credit = self.credit + coin.value;
            if credit >= self.price {
                println!("Thanks, you've got enough: {}", credit);
                State::Go(Open {
                    credit: credit,
                    price: self.price,
                })
            } else {
                println!("Thanks, {} is still missing", self.price - credit);
                State::Wait(Locked {
                    credit: credit,
                    price: self.price,
                })
            }
        }
    }
    

    客户端代码也非常语义化:

    let locked = Locked {
        price: 25,
        credit: 0,
    };
    match locked.step(Coin { value: 5 }) {
        State::Go(open) => {println!("Weeeeeeeeeeeeee!");},
        State::Wait(locked) => {panic!("Oooops");},
    }
    

    我受到Andrew Hobben's Pretty State Machine Pattern的启发 .

相关问题