文章目录

  • 写在前面
  • 实验内容
    • 状态图自动生成(使用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.Equalspublic 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.GetHashCodepublic 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 Falsepriest: 3 devil: 3 boat: False
Next: 3 1 Truepriest: 3 devil: 1 boat: True
Next: 3 2 Falsepriest: 2 devil: 2 boat: True
Next: 3 2 Falsepriest: 3 devil: 2 boat: False
Next: 3 0 Truepriest: 3 devil: 0 boat: True
Next: 3 1 Falsepriest: 3 devil: 1 boat: False
Next: 1 1 Truepriest: 1 devil: 1 boat: True
Next: 2 2 Falsepriest: 2 devil: 2 boat: False
Next: 0 2 Truepriest: 0 devil: 2 boat: True
Next: 0 3 Falsepriest: 0 devil: 3 boat: False
Next: 0 1 Truepriest: 0 devil: 1 boat: True
Next: 1 1 Falsepriest: 1 devil: 1 boat: False
Next: 0 0 Truepriest: 0 devil: 0 boat: True
Next: NULLpriest: 0 devil: 1 boat: False
Next: 0 0 Truepriest: 0 devil: 2 boat: False
Next: 0 0 True

为了更直观地看结果,我按照以上信息,做了一个图:

箭头方向代表寻找最优解的路径方向,每一个状态都有一个最优的转移状态,这也是智能提示所做的工作:帮助玩家从当前状态更快走到结束状态。也就是判断当前玩家的状态,然后根据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学习】魔鬼与牧师过河游戏智能帮助相关推荐

  1. Unity3d入门之路-PD 过河游戏智能帮助

    文章目录 P&D 过河游戏智能帮助 状态图 实现方法 图的表示方法 广度优先搜索 P&D 过河游戏拓展 结果展示 P&D 过河游戏智能帮助 本次作业基本要求是三选一,我选择了P ...

  2. 【Unity 3D学习笔记】PD 过河游戏智能实现

    P&D 过河游戏智能帮助实现 实现状态图的自动生成 讲解图数据在程序中的表示方法 利用算法实现下一步的计算 对于过河游戏,首先需要知道其中各个状态之间的转换关系,绘制状态转移图如下: 其中,P ...

  3. Unity学习之PD 过河游戏智能帮助实现

    Unity学习之P&D 过河游戏智能帮助实现 根据之前设计好的动作分离版过河游戏,我们进行一个简单的状态图AI实现. 转移状态图 状态图老师已经给出: 该状态图只记录了游戏过程中左岸的情况.P ...

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

    3D游戏编程与设计 P&D(牧师与恶魔) 过河游戏智能帮助实现 文章目录 3D游戏编程与设计 P&D(牧师与恶魔) 过河游戏智能帮助实现 一.作业与练习 二.设计简述 1. 状态图基础 ...

  5. Unity3d学习之路-牧师与魔鬼

    Unity3d学习之路-牧师与魔鬼 游戏基本介绍 游戏规则: Priests and Devils is a puzzle game in which you will help the Priest ...

  6. 3D游戏编程实践——PD 过河游戏智能帮助实现

    P&D 过河游戏智能帮助实现 需求 实现状态图的自动生成 讲解图数据在程序中的表示方法 利用算法实现下一步的计算 实现过程 实现状态图的自动生成&讲解图数据在程序中的表示方法 牧师与魔 ...

  7. unity:PD 过河游戏智能帮助实现

    P&D 过河游戏智能帮助实现 github传送门 状态图 状态图课件有 状态图(Statechart Diagram)是描述一个实体基于事件反应的动态行为,显示了该实体如何根据当前所处的状态对 ...

  8. Unity3D 学习笔记4 —— UGUI+uLua游戏框架

    Unity3D 学习笔记4 -- UGUI+uLua游戏框架 使用到的资料下载地址以及基础知识 框架讲解 拓展热更过程 在这里我们使用的是uLua/cstolua技术空间所以提供的UGUI+uLua的 ...

  9. 牧师与恶魔过河游戏——智能提示

    前言 这次实现一个含提示功能的牧师与恶魔过河小游戏,主要在上一个版本的牧师与恶魔小游戏上进行更改,通过增加一个状态计算和改版了得寻路算法,实现向玩家提示如何胜利完成游戏.游戏主体实现思路见上一篇博客- ...

最新文章

  1. 开发高质量软件需要更高成本吗?
  2. Python基础教程:属性值设置和判断变量是否存在
  3. c680和c650_最低10万95,全新F800R、C650Sport和F800GT,BMW三款焕新上市
  4. static在内存层面的作用_static的作用和内存划分?
  5. 《系统集成项目管理工程师》必背100个知识点-58沟通方式
  6. 云上的可观察性数据中台,如何构建?
  7. Object address check - Jurisdiction code
  8. java ibatis 获取执行的sql_小程序官宣+JAVA 三大框架基础面试题
  9. Python基础(十一)--正则表达式
  10. 啥?用了并行流还更慢了
  11. 18kw丹佛斯变频器常见故障_变频器常见故障——输出不平衡、过载、开关电源损坏...
  12. 理解UIScrollView
  13. dataset基本用法
  14. 优化神器 beamoff
  15. PHP+Mysql 实现数据库增删改查
  16. Pygame制作跳跃小球小游戏
  17. 【程序人生】外包公司派遣到网易,上班地点网易大厦,转正后工资8k-10k,13薪,包三餐,值得去吗?
  18. JS JQuery 操作: Json转 Excel 下载文件
  19. 剑指 Offer 05. 替换空格无标题(正则表达式)
  20. 【怎么提高测试质量】

热门文章

  1. JavaWeb - 验证码
  2. Vuex/Vue 练手项目 在线汇率转换器
  3. npp夜光数据介绍 viirs_最新 夜光遥感影像VIIRSDMSP下载总结
  4. 夜间灯光影像区域稳定像元提取
  5. matlab 线性索引 转换,自己编写的 matlab 线性索引转换下标 函数
  6. 简化操作教会你如何使用接口,利用关键词搜索技术获取1688的商品数据
  7. html5 框架angularjs,5款最好用的AngularJS程序构建框架
  8. 中反应器体积_宜兴定做中高压恒流泵_菲立化学
  9. java客户端采集_java实现抽取采集数据的报表工具
  10. PPP协议实现透明传输的2种方法以及工作状态