【Unity3d学习】魔鬼与牧师过河游戏智能帮助

文章目录

  • 写在前面
  • 实验内容
    • 状态图自动生成(使用DFS)
      • 1. 状态表示
      • 2.DFS算法实现
      • 3.DFS生成结果
    • 更改Controller
    • 效果展示

写在前面

  • 本次项目Github地址:传送门
  • 本次项目的视频演示地址(相比之前增加智能提示的步骤):传送门
  • 项目的详细内容见潘老师的课程网站:网站链接
  • 上一个版本的内容,查看我之前的博客:牧师与魔鬼动作分离版

实验内容

  • 实现状态图的自动生成
  • 讲解图数据在程序中的表示方法
  • 利用算法实现下一步的计算
  • 参考:P&D 过河游戏智能帮助实现

(跑过了自己的算法之后,发现在参考博客里面,发现其实有个地方是有错的。它的状态图是用起始岸的魔鬼与牧师数来表示的,另一边岸就可以通过3减去起始岸的角色数量来得到。)
截自参考博客的状态图:
在这里插入图片描述
了解游戏规则都知道,这个状态是不能存在的,因为另一边就是1P2D,游戏结束了。
整个状态图里面,除非两者都是2,否则不可能出现2P这样的状态。这个应该是博主的一个小错误。

状态图自动生成(使用DFS)

自动生成过程可以利用搜索算法来实现,实际上我们都可以知道整个状态图的状态数其实不是很多(毕竟要适合用户玩,游戏难度本来就不太高),所以搜索过程实际上也是很快就能够得出解的。而关键在于如何设计状态的转移,如何将其程序实现实现?首先搜索算法中需要表示每一个状态,然后就是状态到状态的转移的表示,最后就是算法的设计(包括Closed表、最佳路径等的生成)。

1. 状态表示

每一个状态都可以看成由两个部分组成:角色的数量、船的位置。
角色的数量又可以看情况分为:河两岸的人数、每一边牧师数量和魔鬼的数量
由于是深度优先搜索,所以还需要记录到下一个节点的状态,类似树的结构。为了方便各个状态之间的双向转移,可以构建一个双向链表,指向父节点。
结构体如下:

public class State{
    public int priest;
    public int devil;
    public bool boat;
    public State parent; // 记录深搜时从哪一个状态扩展出来,没什么重要用途
    public State best_way; //最佳路径,遍历全部状态后得到一条通向解的路径
    
    public State() {}
    public State(int p, int d, bool b) {
        this.priest = p;
        this.devil = d;
        this.boat = b;
    }

    public State(int p, int d, bool b, State par) {
        this.priest = p;
        this.devil = d;
        this.boat = b;
        this.parent = par;
    }
    public State(State copy) {
        this.priest = copy.priest;
        this.devil = copy.devil;
        this.boat = copy.boat;
        this.parent = copy.parent;
        this.best_way = copy.best_way;
    }

    public bool isEqual(State compare) {
        return this.priest==compare.priest && this.devil==compare.devil && this.boat == compare.boat;
    }

    // override object.Equals
    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }
        State tmp = (State)obj;
        return this.priest==tmp.priest && this.devil==tmp.devil && this.boat == tmp.boat;
    }
    
    // override object.GetHashCode
    public override int GetHashCode()
    {
        throw new System.NotImplementedException();
    }

    public override String ToString() {
        if (best_way == null) {
            return "priest: " + priest.ToString() + " devil: " + devil.ToString() + " boat: " + boat.ToString() + 
        "\nNext: " + "NULL";
        }
        return "priest: " + priest.ToString() + " devil: " + devil.ToString() + " boat: " + boat.ToString() + 
        "\nNext: " + best_way.priest.ToString() + " " + best_way.devil.ToString() + " " + best_way.boat.ToString();
    }
}

这个类定义了上述可以表示状态的一些变量,表示牧师与魔鬼的数量时,只需要记录起始岸边(左边河岸)的数量就好的,因为总数已知,所以可以通过总数减左边河岸得到右边河岸的数量,没有必要再另外存储。
定义了几个不同签名的构造函数,方便创建状态。
重载了Equals函数,便于使用List等集合结构来存储。
重载了ToString函数, 便于打印当前状态的信息。

2.DFS算法实现

DFS只需要从一个状态转移到另一个状态,就需要定义转移的操作。

  1. 可以明确的是一定需要有人在船上,才能发生转移;
  2. 有船的岸边才能载人;
  3. 而且船的转移一定是从一个岸边转移到另一个岸边;

除了以上固定的转移规则,其余的规则定义如下:

  • 一次转移一个牧师/两个牧师
  • 一次转移一个魔鬼/两个魔鬼
  • 一次转移一个魔鬼和一个牧师

转移的方法就是将有船的一边人数减少,另一边人数增加;但是我们状态只记录了左岸的人数,所以当船在左岸时,发生转移则人数减少;当船在右岸时,发生转移则人数增加。

每次深搜就是从这些转移的状态中找一个,继续搜索下去,注意只能是有效状态才能够继续搜索。有效状态的定义就是没有触发游戏结束条件的状态。
算法类代码:

public class AI {
    public static List<State> closed = new List<State>();
    public static State end = new State(0, 0, true);
    public bool isFind = false;

    public bool DFS(ref State root) {
        closed.Add(root);
        if (root.isEqual(end)) {
            isFind = true;
        }
        for (int i = 0; i < 5; i ++) {
            State next = nextState(root, i);
            
            if (next != null) {
                if (closed.Contains(next))
                    continue;

                next.parent = root;
                if (isFind) {
                    next.best_way = root;
                }
                else {
                    closed.Remove(root);
                    root.best_way = next;
                    closed.Add(root);
                }
                
                DFS(ref next);
            }
            
        }
        if (!root.isEqual(end) && root.best_way == null) {
            root.best_way = root.parent;
        }
        return isFind;
    }

    public void print() {
        for (int i = 0; i < closed.Count; i ++) {
            Debug.Log(closed[i].ToString());
        }
    }
    public static bool isValid(State s) {
        if (s.priest != 0 && s.priest < s.devil) { // 左边有牧师且 牧师人数不应少于魔鬼
            return false;
        }
        if (s.priest != 3 && (3-s.priest) < (3-s.devil)) { //右边有牧师且 牧师人数不应少于魔鬼
            return false;
        }
        return true;
    }

    public State nextState(State s, int operation) {
        int p, d;
        bool b;
        p = s.priest;
        d = s.devil;
        b = s.boat;
        State next = null;
        if (b) { // 船在右方
            if (operation == 0) {
                if (3-p >= 1) { // 右方牧师大于1人,可过
                    next = new State(p+1, d, !b);
                }
                else {
                    return null;
                }
            } 
            else if (operation == 1) {
                if (3-p >= 2) { // 右方牧师大于2人,可过
                    next = new State(p+2, d, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 2) {
                if (3-d >= 1) { // 右方魔鬼大于1人,可过
                    next = new State(p, d+1, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 3) {
                if (3-d >= 2) { // 右方魔鬼大于1人,可过
                    next = new State(p, d+2, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 4) {
                if(3-p >= 1 && 3-d >= 1) {
                    next = new State(p+1, d+1, !b);
                }
                else {
                    return null;
                }
            }
        }
        else { // 船在左方
           if (operation == 0) {
                if (p >= 1) {
                    next = new State(p-1, d, !b);
                }
                else {
                    return null;
                }
            } 
            else if (operation == 1) {
                if (p >= 2) {
                    next = new State(p-2, d, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 2) {
                if (d >= 1) {
                    next = new State(p, d-1, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 3) {
                if (d >= 2) {
                    next = new State(p, d-2, !b);
                }
                else {
                    return null;
                }
            }
            else if (operation == 4) {
                if (p >= 1 && d >= 1) {
                    next = new State(p-1, d-1, !b);
                }
                else {
                    return null;
                }
            } 
        }

        if (isValid(next)) {
            return next;
        }

        return null;
    }
}

创建一个closed表,存放已经访问过的节点。搜索过程中,利用list.contain来判断当前状态是否已经访问过,如果访问过就不再拓展。
深搜的过程相信都很熟悉,就不再展开。只不过这里的深搜实际上需要遍历到所有的状态,即使找到了一条正确的状态转移路径也不会马上停止,而是需要找到所有状态,并且找出它下一步的最佳走法。
比如说:一个状态无法在往下扩展,所以他的最佳状态就只能是他的父节点状态。
对于一个父节点,最佳状态就是当前在搜索的那一条路径。如果这条路径又回溯回来,就将最佳状态设为下一条搜索路径。如果这个节点被访问过,但是状态又发生改变的话,就需要从closed表中取出,再重新加入。

至于对下一个状态的寻找,主要是分别根据以上所列几种状态转移来判断,如果没有足够的人转移,则返回;状态生成后,还需要判断牧师与魔鬼的数量是否符合规则,如果不符合返回null。

搜索结束后,得到的结果全部存在了closed表中,closed表中存放的是一个个状态,每个状态都包含了自身信息,以及下一个最佳转移状态。通过这个转移,就可以得到一条通向结果的路径。

3.DFS生成结果

将closed表中的元素全部打印出来得到以下结果:(牧师魔鬼的数量只有在左边岸上的数量,船的状态:False表示在左岸,True表示在右岸)

priest: 3 devil: 2 boat: True
Next: 3 3 False

priest: 3 devil: 3 boat: False
Next: 3 1 True

priest: 3 devil: 1 boat: True
Next: 3 2 False

priest: 2 devil: 2 boat: True
Next: 3 2 False

priest: 3 devil: 2 boat: False
Next: 3 0 True

priest: 3 devil: 0 boat: True
Next: 3 1 False

priest: 3 devil: 1 boat: False
Next: 1 1 True

priest: 1 devil: 1 boat: True
Next: 2 2 False

priest: 2 devil: 2 boat: False
Next: 0 2 True

priest: 0 devil: 2 boat: True
Next: 0 3 False

priest: 0 devil: 3 boat: False
Next: 0 1 True

priest: 0 devil: 1 boat: True
Next: 1 1 False

priest: 1 devil: 1 boat: False
Next: 0 0 True

priest: 0 devil: 0 boat: True
Next: NULL

priest: 0 devil: 1 boat: False
Next: 0 0 True

priest: 0 devil: 2 boat: False
Next: 0 0 True

为了更直观地看结果,我按照以上信息,做了一个图:
【Unity3d学习】魔鬼与牧师过河游戏智能帮助_第1张图片
箭头方向代表寻找最优解的路径方向,每一个状态都有一个最优的转移状态,这也是智能提示所做的工作:帮助玩家从当前状态更快走到结束状态。也就是判断当前玩家的状态,然后根据next来进行转移。

更改Controller

在Controller开始,就通过AI的类,使用DFS计算出所有状态的转移路径,这样就会存在AI类中的closed表里面,随时可以取用。

实现交互功能,首先需要添加一个新的接口,也就是我们新加的功能,并且实现它:

public void getTips() {
    if (forbid) return;
    
    if (boat.getCount()[0] != 0 || boat.getCount()[1] != 0) {
        for (int i = 0; i < 2; i ++) {
            if (boat.getChar(i) != null)
                setCharacterPosition(boat.getChar(i));
        }
    }

    int[] count = leftBank.getCount();
    int d = count[0];
    int p = count[1];
    bool b = boat.getLR()==1;
    State current = new State(p,d,b);
    State next = AI.closed.Find((State s) => {return s.isEqual(current);}).best_way;
    Debug.Log("current: " + current);
    Debug.Log("next: " + next);
    if (next == null) return;
    if (b) {
        int d2 = next.devil - d;
        int p2 = next.priest - p;
        while (d2 > 0 || p2 > 0) {
            for (int i = 0; i < 6; i ++) {
                if (characters[i].getBank() != null && characters[i].getBank().getLR() == 1) {
                    if (d2 > 0 && characters[i].getMan() == "Devil") {
                        setCharacterPosition(characters[i]);
                        d2 --;
                        break;
                    }
                    if (p2 > 0 && characters[i].getMan() == "Priest") {
                        setCharacterPosition(characters[i]);
                        p2 --;
                        break;
                    }
                }
                if (i==5){
                    Debug.Log("Err");
                    return;
                }
            }
        }
    }
    else {
        int d2 = -next.devil + d;
        int p2 = -next.priest + p;
        while (d2 > 0 || p2 > 0) {
            for (int i = 0; i < 6; i ++) {
                if (characters[i].getBank() != null && characters[i].getBank().getLR() == 0) {
                    if (d2 > 0 && characters[i].getMan() == "Devil") {
                        setCharacterPosition(characters[i]);
                        d2 --;
                        break;
                    }
                    if (p2 > 0 && characters[i].getMan() == "Priest") {
                        setCharacterPosition(characters[i]);
                        p2 --;
                        break;
                    }
                }
                if (i==5){
                    Debug.Log("Err");
                    return;
                }
            }
        }
    }
    MoveBoat();
}

首先统计当前人数以确定当前状态,为了方便统计,所以需要先把船上的角色先重新移回岸上(之前的接口设计不完善),由于之前移动角色是用到了动作,有一个时间的问题,这里代码是连续执行的,就会起矛盾,因为这里直接调用了move的动作函数,但是又不能直接设置回调,所以难以修改。所以新建了一个函数,直接改变角色的位置,取消了动作执行的过程。
统计人数并且得出状态后,根据状态的next,构建一个目标状态,根据这个目标的状态选择上下船的人数,最后执行moveBoat()完成一次提示。而这个接口可以绑定在UI的一个按钮上(使用IMGUI实现),然后按钮被调用就执行提示。

效果展示

你可能感兴趣的:(Unity3d学习)