3D游戏编程与设计 P&D(牧师与恶魔)过河游戏智能帮助实现

3D游戏编程与设计 P&D(牧师与恶魔) 过河游戏智能帮助实现

牧师与恶魔(游戏智能版) 完整游戏过程可见以下视频:
https://www.bilibili.com/video/BV1BV411h7oW/

牧师与恶魔(游戏智能版) 完整代码可见以下仓库:
https://gitee.com/beilineili/game3-d/tree/master/10.PD_AI

一、作业与练习

P&D 过河游戏智能帮助实现,程序具体要求:

二、设计简述

  • 游戏智能,可以理解为在游戏规则约束下,通过适当的算法使得游戏中 NPC 呈现为具有一定人类智能行为的博弈对手,让游戏玩家面临不间断的挑战,并在挑战中有所收获。接下来我们将尝试在《牧师与恶魔》游戏中实现智能
  • 为了帮助小朋友玩 P&D 过河,决定开发 next 功能,提示下一步最佳玩法?但怎么设计呢?

因此,需要将牧师与魔鬼过河问题抽象为状态图:

1. 状态图基础

  • 状态图是描述一个实体基于事件反应的动态行为,显示了该实体如何根据当前所处的状态对不同的事件做出反应。

2. 状态图表示方式

  • 状态图数据如何在代码里表示?
  • 可以用邻接矩阵或邻接表。笔者选用了邻接表来实现,邻接表是图的一种链式存储结构。比如对于图 G 中的每个顶点 vi,把所有邻接 vi 的顶点 vj 连接成一个链表,这个链表就是节点 vi 的邻接表。

3. 牧师与恶魔状态图

  • 以下参考了师兄博客里的状态图
    在这里插入图片描述
  • 该状态图记录了游戏过程中左岸的情况。其中 P 代表 Priest 牧师,D 代表 Devil 魔鬼,B 代表 Boat 船。当船在右岸时不记录。双箭头代表两个状态可以相互转化。
  • 观察状态图:
  • 开始状态 [3P3DB];成功状态 [0P0D];失败状态 *
  • 中间状态 [xPyDX], 其中 x>=y and [(3-x)>=(3-y) or (3-x)==0]
  • 可能动作:{P,D,PP,PD,DD}
  • 问题求解:
  • 对于任意非成功/失败状态,找一条最短路径到达成功状态

  • 在牧师与恶魔这个游戏中,实现游戏智能的方式是先创建一个有向图,每次要提示就通过当前状态来找到在图中的位置,分析当前场景的状态,随后读取能到达终点路径上的下一个状态,执行相应动作以达到下一个状态。
  • 牧师数目和恶魔数目的取值范围均是 [0,3],即每个身份的数目取值可能个数为4。以初始状态为例:3P3DB。代表左岸有3个牧师,3个魔鬼,船也在左岸。下一个状态有4种,其中一个执行后会输掉游戏,忽略。在剩下三个状态中,只有 2P2D 和 3P1D 是成功路径上的状态,可以随机任选一个

4. 路径搜索方法

  • 笔者选择宽度优先搜索算法 BFS,它的基本原理如下:
  • 从初始结点开始,生成第一层结点,检查目标结点是否在这些结点中,如果没有,再将所有这一层的结点逐一扩展,得到第二层结点,如此依次扩展,直到发现目标结点为止

  • 它的算法流程如下:
  • 当前状态进入队列。
  • 队列不为空:队头出队。
  • 访问状态节点。
  • 当状态节点为结束状态时,回溯至开始状态;否则计算下一个合法状态并压入队列。

三、代码设计

1. 游戏对象的位置 Position

  • 对比之前实现的 牧师与恶魔普通版,现在将游戏中对象的位置统一用一个类来存储,方便修改对象的位置
  • 包括左右两侧陆地的位置,河水位置,船在左右侧的位置,角色分别在陆地和船上的位置
// 储存游戏当中对象的位置
public class Position
{
    // 右侧岸的位置
    public static Vector3 rightLand = new Vector3(15, -4, 0);

    // 左侧岸的位置
    public static Vector3 leftLand = new Vector3(-13, -4, 0);

    // 河流的位置
    public static Vector3 water = new Vector3(1, -(float) 5.5, 0);

    // 船在右边的位置
    public static Vector3 rightBoat = new Vector3(7, -(float) 3.8, 0);

    // 船在左边的位置
    public static Vector3 leftBoat = new Vector3(-5, -(float) 3.8, 0);

    // 角色在陆地上的相对位置
    public static Vector3[]
        landCharacters =
            new Vector3[] {
                new Vector3((float) - 0.2, (float) 0.7, 0),
                new Vector3((float) - 0.1, (float) 0.7, 0),
                new Vector3(0, (float) 0.7, 0),
                new Vector3((float) 0.1, (float) 0.7, 0),
                new Vector3((float) 0.2, (float) 0.7, 0),
                new Vector3((float) 0.3, (float) 0.7, 0)
            };

    // 角色在船上的相对位置
    public static Vector3[]
        boatCharacters =
            new Vector3[] {
                new Vector3((float) - 0.1, (float) 1.2, 0),
                new Vector3((float) 0.2, (float) 1.2, 0)
            };
}

2. 裁判 JudgeController

  • 裁判类用于检测当前游戏是否胜利或者失败,如果游戏结束则通知 FirstController 处理
public class JudgeController : MonoBehaviour
{
    public FirstController mainController;

    public Land leftLand;

    public Land rightLand;

    public Boat boat;

    void Start()
    {
        mainController =
            (FirstController) SSDirector.GetInstance().CurrentSenceController;
        this.leftLand = mainController.leftLandController.GetLand();
        this.rightLand = mainController.rightLandController.GetLand();
        this.boat = mainController.boatController.GetBoat();
    }

    public void Init()
    {
        mainController =
            (FirstController) SSDirector.GetInstance().CurrentSenceController;
        this.leftLand = mainController.leftLandController.GetLand();
        this.rightLand = mainController.rightLandController.GetLand();
        this.boat = mainController.boatController.GetBoat();
    }

    void Update()
    {
        if (!mainController.isRuning) return;

        this.gameObject.GetComponent<UserGUI>().gameMessage = "";

        // 判断是否已经胜利
        if (rightLand.priestNum == 3)
        {
            mainController.JudgeCallback(false, "You Win!");
            return;
        }
        else
        {
            // 判断是否已经失败
            int
                leftPriestNum,
                leftDevilNum,
                rightPriestNum,
                rightDevilNum;
            leftPriestNum =
                leftLand.priestNum + (boat.isRight ? 0 : boat.priestNum);
            leftDevilNum =
                leftLand.devilNum + (boat.isRight ? 0 : boat.devilNum);

            // 若任意一侧,牧师数量不为 0 且牧师数量少于恶魔数量,则游戏失败
            if (leftPriestNum != 0 && leftPriestNum < leftDevilNum)
            {
                mainController.JudgeCallback(false, "You Lose!");
                return;
            }
            rightPriestNum =
                rightLand.priestNum + (boat.isRight ? boat.priestNum : 0);
            rightDevilNum =
                rightLand.devilNum + (boat.isRight ? boat.devilNum : 0);
            if (rightPriestNum != 0 && rightPriestNum < rightDevilNum)
            {
                mainController.JudgeCallback(false, "You Lose!");
                return;
            }
        }
    }
}


3. 路径节点 RouteNode

  • 将左右两岸的牧师、魔鬼的数量以及船的位置看作为游戏状态,我们将游戏状态封装成一个状态节点类,用于维护游戏状态。RouteNode 是路径上的节点,也是基本状态单元

  • 设置常量表示各个参数

    // 船上牧师
    public static readonly int BOAT_PRIESTS = 0;
    // 船上恶魔
    public static readonly int BOAT_DEVILS = 1;
    // 左岸牧师
    public static readonly int LEFT_PRIESTS = 2;
    // 右岸牧师
    public static readonly int RIGHT_PRIESTS = 3;
    // 左岸恶魔
    public static readonly int LEFT_DEVILS = 4;
    // 右岸恶魔
    public static readonly int RIGHT_DEVILS = 5;
    // 船的位置
    public static readonly int BOAT_PLACE = 6;
    // 保存状态参数
    public int[] state;
  • 获得当前状态,0,1,2 分别表示 游戏进行,游戏胜利,游戏结束
    public int GetState()
    {
        // 所有牧师都到了右岸
        if (state[RIGHT_PRIESTS] == 3)
        {
            return 1;
        }

        int
            leftPriests,
            leftDevils,
            rightPriests,
            rightDevils;
        leftPriests =
            state[LEFT_PRIESTS] + state[BOAT_PRIESTS] * (1 - state[BOAT_PLACE]);
        leftDevils =
            state[LEFT_DEVILS] + state[BOAT_DEVILS] * (1 - state[BOAT_PLACE]);
        rightPriests =
            state[RIGHT_PRIESTS] + (state[BOAT_PRIESTS] * state[BOAT_PLACE]);
        rightDevils =
            state[RIGHT_DEVILS] + (state[BOAT_DEVILS] * state[BOAT_PLACE]);

        // 有一侧的牧师数量少于恶魔
        if (
            (leftPriests != 0 && leftPriests < leftDevils) ||
            (rightPriests != 0 && rightPriests < rightDevils)
        )
        {
            return 2;
        }

        // 游戏还未结束
        return 0;
    }
  • 用两个指针分别用来记录路径的下一步和上一步
    // 父节点
    RouteNode parentNode;
    // 下一节点
    RouteNode nextNode;
  • 节点可以用状态数组或其他节点初始构造
    public RouteNode(int[] state)
    {
        parentNode = null;
        nextNode = null;
        this.state = (int[]) state.Clone();
    }

    public RouteNode(RouteNode other)
    {
        parentNode = other;
        nextNode = null;
        state = (int[]) other.state.Clone();
    }
  • 判断状态节点的变更是否有效
        // 是否可以移动
    public bool isValid(int currentState)
    {
        if (currentState == LEFT_PRIESTS || currentState == LEFT_DEVILS)
        {
            return (
            state[BOAT_PLACE] == 0 &&
            (state[BOAT_PRIESTS] + state[BOAT_DEVILS] < 2) &&
            state[currentState] > 0
            );
        }
        else if (currentState == RIGHT_PRIESTS || currentState == RIGHT_DEVILS)
        {
            return (
            state[BOAT_PLACE] == 1 &&
            (state[BOAT_PRIESTS] + state[BOAT_DEVILS] < 2) &&
            state[currentState] > 0
            );
        }
        else if (currentState == BOAT_PRIESTS || currentState == BOAT_DEVILS)
        {
            return (state[currentState] > 0);
        }
        else if (currentState == BOAT_PLACE)
        {
            return (state[BOAT_PRIESTS] + state[BOAT_DEVILS] > 0);
        }
        return false;
    }
  • 在状态图的节点上移动函数
    public bool Step(int state)
    {
        bool isvalid = isValid(currentState);
        if (!isvalid) return false;
        if (currentState == LEFT_PRIESTS || currentState == RIGHT_PRIESTS)
        {
            state[currentState]--;
            state[BOAT_PRIESTS]++;
        }
        else if (currentState == LEFT_DEVILS || currentState == RIGHT_DEVILS)
        {
            state[currentState]--;
            state[BOAT_DEVILS]++;
        }
        else if (currentState == BOAT_PRIESTS)
        {
            state[currentState]--;
            state[state[BOAT_PLACE] == 0 ? LEFT_PRIESTS : RIGHT_PRIESTS]++;
        }
        else if (currentState == BOAT_DEVILS)
        {
            state[currentState]--;
            state[state[BOAT_PLACE] == 0 ? LEFT_DEVILS : RIGHT_DEVILS]++;
        }
        else if (currentState == BOAT_PLACE)
        {
            state[BOAT_PLACE] = 1 - state[BOAT_PLACE];
        }
        return true;
    }

4. 寻找路径 RouteController

  • 主要通过广度优先搜索算法 BFS 来寻找到达游戏胜利结果的状态路径
  • 将每一个访问状态结点放入队列,每一个结点都可以到下一个节点或返回父亲结点,到最后可以生成一条到达目标状态的路径。
  • 首先判断当前节点是否是终点,是则返回状态路径中的下一步
  • 若不是,则将当前节点移出队列,遍历它的各种移动方式,将它的邻居节点(移动有效且还未访问过的)放入队列,再随机选取其中一个作为新的初始节点
  • 如果队列不为空,则重复以上步骤;如果为空且当前节点不是终点,则无解
    // 路径的当前状态节点
    protected RouteNode currentNode;

    // 保存从起始状态到达目标状态路径中的每一个状态节点
    private Hashtable solutionRoute;

    // 保存已发现但未访问的节点
    private Queue<RouteNode> exploreList;

    // 保存已发现的节点
    private HashSet<int> visitedList;
  • 以上广度优先搜索算法 BFS 的具体实现
public RouteNode BFSSearch(int[] nowState)
    {
        RouteNode destination = new RouteNode(nowState);

        // 如果已经获得路径,则返回结果
        if (
            solutionRoute != null &&
            solutionRoute.Contains(destination.GetHashCode())
        ) return (RouteNode) solutionRoute[destination.GetHashCode()];
        currentNode = null;
        solutionRoute = null;
        exploreList = new Queue<RouteNode>();
        visitedList = new HashSet<int>();
        exploreList.Enqueue (destination);
        visitedList.Add(destination.GetHashCode());

        // 广度优先搜索
        while (exploreList.Count != 0)
        {
            currentNode = exploreList.Peek();
            if (currentNode.GetState() == 1)
            {
                GenerateRoute();
                break;
            }
            exploreList.Dequeue();

            RouteNode[] nextNodes =
                new RouteNode[] {
                    new RouteNode(currentNode),
                    new RouteNode(currentNode),
                    new RouteNode(currentNode),
                    new RouteNode(currentNode),
                    new RouteNode(currentNode),
                    new RouteNode(currentNode),
                    new RouteNode(currentNode)
                };

            // 寻找所有与currentNode邻接且未访问的节点,添加到 exploreList
            // 并加入已访问节点队列 visitedList
            for (int i = 0; i < 7; i++)
            {
                if (nextNodes[i].Step(i))
                {
                    if (
                        nextNodes[i].GetState() <= 1 &&
                        !visitedList.Contains(nextNodes[i].GetHashCode())
                    )
                    {
                        exploreList.Enqueue(nextNodes[i]);
                        visitedList.Add(nextNodes[i].GetHashCode());
                    }
                }
            }
        }
        return destination;
    }
  • 绘制路径,从终点沿着父节点一路回溯到起点,将这条解决问题的路径用 solutionRoute 存储
    public void GenerateRoute()
    {
        if (solutionRoute == null && currentNode != null)
        {
            solutionRoute = new Hashtable();
            RouteNode
                cNode,
                lNode;
            cNode = currentNode;
            lNode = null;
            while (cNode != null)
            {
                solutionRoute.Add(cNode.GetHashCode(), cNode);
                cNode.SetNext (lNode);
                lNode = cNode;
                cNode = cNode.GetParent();
            }
        }
    }

5. 主控制器 FirstController

  • 增加 ChangeState 函数,输入两个状态节点(起点和终点),完成状态转换,改变游戏对象的参数(位置移动)
private void ChangeState(RouteNode sourceNode, RouteNode destinationNode)
    {
        int[] change = new int[7];

        // 得到转换状态
        for (int i = 0; i < 7; i++)
        {
            change[i] = destinationNode.state[i] - sourceNode.state[i];
        }
        if (change[RouteNode.BOAT_PLACE] != 0)
        {
            // 船只移动
            MoveBoat();
        }
        else if (change[RouteNode.BOAT_PRIESTS] < 0)
        {
            // 船上牧师移动
            for (int i = 0; i < 2; i++)
            {
                if (
                    boatController.GetBoat().characters[i] != null &&
                    boatController.GetBoat().characters[i].isPriest
                )
                {
                    MoveCharacter(boatController.GetBoat().characters[i]);
                    break;
                }
            }
        }
        else if (change[RouteNode.BOAT_DEVILS] < 0)
        {
            // 船上恶魔移动
            for (int i = 0; i < 2; i++)
            {
                if (
                    boatController.GetBoat().characters[i] != null &&
                    !boatController.GetBoat().characters[i].isPriest
                )
                {
                    MoveCharacter(boatController.GetBoat().characters[i]);
                    break;
                }
            }
        }
        else if (change[RouteNode.LEFT_PRIESTS] < 0)
        {
            //左岸牧师移动
            for (int i = 0; i < 6; i++)
            {
                if (
                    characterControllers[i].GetCharacter().isPriest &&
                    !characterControllers[i].GetCharacter().isInBoat &&
                    !characterControllers[i].GetCharacter().isRight
                )
                {
                    MoveCharacter(characterControllers[i].GetCharacter());
                    break;
                }
            }
        }
        else if (change[RouteNode.LEFT_DEVILS] < 0)
        {
            //左岸恶魔移动
            for (int i = 0; i < 6; i++)
            {
                if (
                    !characterControllers[i].GetCharacter().isPriest &&
                    !characterControllers[i].GetCharacter().isInBoat &&
                    !characterControllers[i].GetCharacter().isRight
                )
                {
                    MoveCharacter(characterControllers[i].GetCharacter());
                    break;
                }
            }
        }
        else if (change[RouteNode.RIGHT_PRIESTS] < 0)
        {
            //右岸牧师移动
            for (int i = 0; i < 6; i++)
            {
                if (
                    characterControllers[i].GetCharacter().isPriest &&
                    !characterControllers[i].GetCharacter().isInBoat &&
                    characterControllers[i].GetCharacter().isRight
                )
                {
                    MoveCharacter(characterControllers[i].GetCharacter());
                    break;
                }
            }
        }
        else if (change[RouteNode.RIGHT_DEVILS] < 0)
        {
            //右岸恶魔移动
            for (int i = 0; i < 6; i++)
            {
                if (
                    !characterControllers[i].GetCharacter().isPriest &&
                    !characterControllers[i].GetCharacter().isInBoat &&
                    characterControllers[i].GetCharacter().isRight
                )
                {
                    MoveCharacter(characterControllers[i].GetCharacter());
                    break;
                }
            }
        }
    }
  • 增加 NextStep 函数,将当前的各个游戏对象的数量、位置,转换为状态传给 RouteController 生成路径,然后根据这条路径上当前节点和下一节点,作为参数传给 ChangeState 进行状态转换,改变游戏对象的参数
public void NextStep()
    {
        RouteNode node =
            RouteNodeController
                .BFSSearch(new int[] {
                    boatController.GetBoat().priestNum,
                    boatController.GetBoat().devilNum,
                    leftLandController.GetLand().priestNum,
                    rightLandController.GetLand().priestNum,
                    leftLandController.GetLand().devilNum,
                    rightLandController.GetLand().devilNum,
                    boatController.GetBoat().isRight ? 1 : 0
                });
        if (node.GetState() == 0)
        {
            RouteNode nNode = node.GetNext();
            ChangeState (node, nNode);
        }
    }

四、游戏效果

  • 以上代码增加了一个 “下一步” 按钮,当游戏未结束且用户点击此按钮时,执行 nextMove,完成下一步通往游戏胜利的状态移动。
    在这里插入图片描述
  • 全程按 “下一步” 按钮可以获得游戏胜利
    在这里插入图片描述

五、心得

  • 这次作业让我意识到,利用好状态图,是实现游戏智能的一种有效方法,当然这也是建立在牧师与恶魔游戏的游戏结果明确,中间过程状态数较少的前提下,如果游戏更加复杂还能不能用状态图来模拟游戏智能就不一定了。
  • 总的来说,这次练习只是用了最简单的 if-else 来实现开发者事先做好的状态图,远远不是真正意义上的人工智能,对于玩家看来它非常神奇,觉得机器真聪明。但对于开发者,这种所谓的 “智能技术” 就是忽悠人
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值