牧师与恶魔过河游戏——智能提示
前言
这次实现一个含提示功能的牧师与恶魔过河小游戏,主要在上一个版本的牧师与恶魔小游戏上进行更改,通过增加一个状态计算和改版了得寻路算法,实现向玩家提示如何胜利完成游戏。游戏主体实现思路见上一篇博客——牧师与恶魔小游戏 动作分离版。
游戏效果图如下:
状态设置
这里的状态是指右边岸上的牧师数目和恶魔数目,易知道牧师数目和恶魔数目的取值范围均是[0,3],即每个身份的数目取值可能个数为4,设P为牧师,D为恶魔,p为牧师数目,d为恶魔数目,则(p,d)的可能取值为4 * 4 = 16种,即(p,d)即为一个状态,例如(2,1)表示有两个牧师,一个魔鬼。而这个游戏,实质上是进行状态之间的转换,经过一系列的状态转换,使得状态最终从(3,3)变为(0,0)。
接下来就是这些状态怎么表示的问题了。考虑到需要进行状态的转换,于是,用二维矩阵来表示,其中,每行的下标表示当前状态,列的取值表示能否转换至下一状态,上面已经说了有16中状态,所以,可用边长为16的二维矩阵表示。如下图:
在开始写代码之前,得明确以下两点:
二维矩阵下标和状态的转换关系: 已经确定状态表示为(p,d),其中,p和d的取值范围均为[0,3],在这里,用index代表数组下标,则,转换关系可为:
p = index / 4;
d = index % 4;
两个状态之间可转换的条件:
1. 由于船每次航行最多只能有两个人,最少必须有一个人,所以,船上人的状态可能为(1,1)、(1,0)、(0,1)、(2,0)、(0,2),而我们现在讨论的是右边岸上的状态,则根据船是离岸或上岸,右岸状态可能是加上或减去上述五个状态,而通过前面的式子,可以计算出,对应的下标变换为{ 5, 4, 1, 8, 2 }。
2. 两岸和船上牧师的总数必须为3;两岸和船上恶魔的总数必须为3。(这一点我一开始没有想到,花费了挺多时间来找bug。。。)
3. 下一状态中,当岸上有牧师时,岸上(含船)牧师的数目必须不少于恶魔的数目。在这里可以作个变换,当两边岸上都有牧师时,为了保持牧师的数目不少于恶魔的数目,则必须两岸上,牧师的数目等于恶魔的数目,而当其中一个岸边没有牧师时,则无需这个限制,所以,当pristNumber != 0 && pristNumber != 3 && pristNumber != devilNumber条件成立时,则不符合要求。
根据上述的两个条件以及状态的表示方式,则可以将所有状态与其可转换的下一状态关联起来,形成一个二维关系矩阵,如下:
画成状态转换图,如下:
具体实现代码如下:
private bool[][] graph = new bool[16][];
//(P,D):(1,1)(1,0)(0,1)(2,0)(0,2)private int[] moveSteps = { 5, 4, 1, 8, 2 };//状态初始化public void init(){for(int i = 0; i < 16; i++){graph[i] = new bool[16];for (int j = 0; j < 16; j++)graph[i][j] = false;}for(int i = 0; i < 16; i++){for(int j = 0; j < 5; j++){int opp = 15 - i;if (i + moveSteps[j] >= 0 && i + moveSteps[j] < 16){if ((((opp - moveSteps[j]) % 4 + i % 4 + moveSteps[j] % 4) == 3) && (((opp - moveSteps[j]) / 4 + i / 4 + moveSteps[j] / 4) == 3))graph[i][i + moveSteps[j]] = true;}if (i - moveSteps[j] >= 0 && i - moveSteps[j] < 16){if ((opp % 4 + (i - moveSteps[j]) % 4 + moveSteps[j] % 4) == 3 && (opp / 4 + (i - moveSteps[j]) / 4 + moveSteps[j] / 4 == 3))graph[i][i - moveSteps[j]] = true;}}}for(int i = 0; i < 16; i++){//根据下一状态确定连线for(int j = 0; j < 16; j++){int pristNumber = j / 4;int devilNumber = j % 4;if(pristNumber != 0 && pristNumber != 3 && pristNumber != devilNumber){graph[i][j] = false;}}}}
寻找路径
在生成了状态转换图后,接下来的工作就是寻找从任意状态到终点状态的路径了,事实上,很多状态是不会发生的。本来以为寻找状态路径和寻找图的路径一样,后来才发现,由于船的位置,可能在右边,也可能在左边,便不能单纯地当图来处理了。在这个时候,假设船的位置与岸上的状态无关,则有32种状态,除了岸上本身的16中状态,再加上船的位置的2种状态。虽然船的位置在一定程度上和岸上状态有关联,但在这里进行简化处理,以32种状态进行计算。用一个二维数组来记录状态是否已被访问,其中,第一维表示岸上状态,第二维表示船的位置。然后,采用深度优先搜索算法,用一个ArrayList结构来模拟栈结构,记录路径。
主要注意两点:
1. 每次进入下一个状态,船的位置必须改变。而状态出栈的时候,也要记得将船的位置状态改变回来。
2. 船的位置不同,导致当前状态转换至下一状态的条件也发生改变。用current表示当前状态,next表示下一状态,则,当船在右边的时候,下一状态牧师和恶魔的数目肯定减少,current%4 <= next%4 且 current / 4 <= next / 4,即current < next;当船在左边时,下一状态牧师和恶魔的数目增加,current % 4 >= next %4 且 current / 4 >= next / 4, current > next。因此,可通过current值下标为分界线,根据船的不同位置,改变可访问的下一状态。
代码如下:
public ArrayList findPath(int pristNumber, int devilNumber, int boatPosition){bool[][] visited = new bool[16][];for(int i = 0; i < 16; i++){visited[i] = new bool[]{ false, false};}ArrayList path = new ArrayList();int start = pristNumber * 4 + devilNumber;path.Add(start);while(true){// Debug.Log("start" + start);bool hasChild = false;if (boatPosition == 1){visited[start][0] = true;//(start/4 >= i/4) && (start%4 >= i%4)for (int i = 0; i < start; i++){if(graph[start][i] && !visited[i][1]){path.Add(i);hasChild = true;// Debug.Log("here - right");break;}}}else{visited[start][1] = true;for(int i = start; i < 16; i++){if(graph[start][i] && !visited[i][0]){path.Add(i);hasChild = true;// Debug.Log("here - left");break;}}}if ((int)path[path.Count - 1] == 0) return path;boatPosition = -boatPosition;if(!hasChild) path.RemoveAt(path.Count - 1);start = (int)path[path.Count - 1]; }}
显示
在寻找出路径之后,需要的就是告诉玩家这个路径。当玩家点击提示按钮后,将路径用文字形式告诉玩家,至于是否选择该路径,由玩家决定。在controller类中增加tips()方法,用于传达当前右岸的状态和船的位置。然后,在ClickGUI类中,添加提示按钮,同时,负责将状态路径转换为用户看得懂的方式。主要思路是根据当前状态和下一状态,确定船当前的位置,很容易知道,当当前状态值大于下一状态值时,船在右边,否则,船在左边。而通过前面那两条状态值和(p,d)之间转换的式子,可以求出p和d的值。最后将结果显示出来即可,代码如下:
//处理提示if(GUI.Button(new Rect(10, 10, 120, 50), "Tips", buttonStyle) && !showTips){ArrayList path = (Director.getInstance().currentSceneController as FirstController).tips();int stepCount = 0;if(path.Count == 1){pathStr += "No Tips";} else{for(int i = 0; i < path.Count - 1; i++){bool isBoatOnRight = (int)path[i] > (int)path[i + 1] ? true: false;int temp = isBoatOnRight ? (int)path[i] - (int)path[i + 1] : (int)path[i + 1] - (int)path[i];pathStr += "step_";pathStr += ++stepCount;//(P,D):(1,1)(1,0)(0,1)(2,0)(0,2)switch (temp){case 5:pathStr += ": (prist, devil) ";break;case 4:pathStr += ": (prist, ) ";break;case 1:pathStr += ": (devil, ) ";break;case 8:pathStr += ": (prist, prist) ";break;case 2:pathStr += ": (devil, devil) ";break; default:Debug.Log("something wrong!");break;}pathStr += isBoatOnRight ? "from right to left\n" : "from left to right\n"; }}showTips = true;}if (showTips){GUIStyle fontStyle = new GUIStyle("button");fontStyle.alignment = TextAnchor.MiddleCenter;fontStyle.fontSize = 15;fontStyle.normal.textColor = Color.white;if (GUI.Button(new Rect(50, 50, Screen.width - 100, Screen.height - 100), pathStr, fontStyle)){showTips = false;pathStr = "";}// Debug.Log(pathStr);}
其他
演示视频
改进方法:
- 可改变状态的表示方法,例如用pair类型,看起来更舒服
- 可使用BFS+或者Dijkstra算法来求最短路径。但是在这个游戏中,用DFS即可,结果一样。
已更改的类的代码:
public class AItips {private static AItips _instance;private bool[][] graph = new bool[16][];//(P,D):(1,1)(1,0)(0,1)(2,0)(0,2)private int[] moveSteps = { 5, 4, 1, 8, 2 };public static AItips GetInstance(){if(_instance == null){_instance = new AItips();}return _instance;}//状态初始化public void init(){for(int i = 0; i < 16; i++){graph[i] = new bool[16];for (int j = 0; j < 16; j++)graph[i][j] = false;}for(int i = 0; i < 16; i++){for(int j = 0; j < 5; j++){int opp = 15 - i;if (i + moveSteps[j] >= 0 && i + moveSteps[j] < 16){if ((((opp - moveSteps[j]) % 4 + i % 4 + moveSteps[j] % 4) == 3) && (((opp - moveSteps[j]) / 4 + i / 4 + moveSteps[j] / 4) == 3))graph[i][i + moveSteps[j]] = true;}if (i - moveSteps[j] >= 0 && i - moveSteps[j] < 16){if ((opp % 4 + (i - moveSteps[j]) % 4 + moveSteps[j] % 4) == 3 && (opp / 4 + (i - moveSteps[j]) / 4 + moveSteps[j] / 4 == 3))graph[i][i - moveSteps[j]] = true;}}}for(int i = 0; i < 16; i++){//根据下一状态确定连线for(int j = 0; j < 16; j++){int pristNumber = j / 4;int devilNumber = j % 4;if(pristNumber != 0 && pristNumber != 3 && pristNumber != devilNumber){graph[i][j] = false;}}}}public ArrayList findPath(int pristNumber, int devilNumber, int boatPosition){bool[][] visited = new bool[16][];for(int i = 0; i < 16; i++){visited[i] = new bool[]{ false, false};}ArrayList path = new ArrayList();int start = pristNumber * 4 + devilNumber;path.Add(start);while(true){// Debug.Log("start" + start);bool hasChild = false;if (boatPosition == 1){visited[start][0] = true;//(start/4 >= i/4) && (start%4 >= i%4)for (int i = 0; i < start; i++){if(graph[start][i] && !visited[i][1]){path.Add(i);hasChild = true;// Debug.Log("here - right");break;}}}else{visited[start][1] = true;for(int i = start; i < 16; i++){if(graph[start][i] && !visited[i][0]){path.Add(i);hasChild = true;// Debug.Log("here - left");break;}}}if ((int)path[path.Count - 1] == 0) return path;boatPosition = -boatPosition;if(!hasChild) path.RemoveAt(path.Count - 1);start = (int)path[path.Count - 1]; }}
}
public class ClickGUI : MonoBehaviour {UserAction action;ChaController chac;bool showTips = false;string pathStr = "";//设置点击的人物public void setController(ChaController chaController){chac = chaController;}private void Start(){action = Director.getInstance().currentSceneController as UserAction;}//鼠标点击事件private void OnMouseUp(){try{action.isClickCha(chac);}catch(Exception e){Debug.Log(e.ToString());}finally{Debug.Log("Clicking:" + gameObject.name);}}private void OnGUI(){GUIStyle buttonStyle = new GUIStyle("button");buttonStyle.fontSize = 30;buttonStyle.normal.textColor = Color.blue;//开船事件。由于用鼠标点击的时候,总是点不中,就直接用button来了if ((Director.getInstance().currentSceneController as FirstController).isOver() == 0){if (GUI.Button(new Rect(Screen.width / 2 - 60, Screen.height / 2 - 25, 120, 50), "set sail", buttonStyle)){action.moveBoat();}}//处理提示if(GUI.Button(new Rect(10, 10, 120, 50), "Tips", buttonStyle) && !showTips){ArrayList path = (Director.getInstance().currentSceneController as FirstController).tips();int stepCount = 0;if(path.Count == 1){pathStr += "No Tips";} else{for(int i = 0; i < path.Count - 1; i++){bool isBoatOnRight = (int)path[i] > (int)path[i + 1] ? true: false;int temp = isBoatOnRight ? (int)path[i] - (int)path[i + 1] : (int)path[i + 1] - (int)path[i];pathStr += "step_";pathStr += ++stepCount;//(P,D):(1,1)(1,0)(0,1)(2,0)(0,2)switch (temp){case 5:pathStr += ": (prist, devil) ";break;case 4:pathStr += ": (prist, ) ";break;case 1:pathStr += ": (devil, ) ";break;case 8:pathStr += ": (prist, prist) ";break;case 2:pathStr += ": (devil, devil) ";break; default:Debug.Log("something wrong!");break;}pathStr += isBoatOnRight ? "from right to left\n" : "from left to right\n"; }}showTips = true;}if (showTips){GUIStyle fontStyle = new GUIStyle("button");fontStyle.alignment = TextAnchor.MiddleCenter;fontStyle.fontSize = 15;fontStyle.normal.textColor = Color.white;if (GUI.Button(new Rect(50, 50, Screen.width - 100, Screen.height - 100), pathStr, fontStyle)){showTips = false;pathStr = "";}// Debug.Log(pathStr);}//加点儿游戏的文字说明GUIStyle gUIStyle = new GUIStyle();gUIStyle.fontSize = 30;gUIStyle.normal.textColor = Color.green;GUI.Label(new Rect(Screen.width / 2 - 150, 10, 300, 40), "Priests And Devils", gUIStyle);GUI.Label(new Rect(5, 80, 250, 300), "Priests and Devils is a " +"game in which you will help the Priests and Devils to cross the river." +" There are 3 priests and 3 devils at one side of the river." +" They all want to get to the other side of this river, " +"but there is only one boat and this boat can only carry two persons each time." +" And there must be one person steering the boat from one side to the other side." +" In this game, you can click on them to move them and click the button to move the boat to the other direction. " +"If the priests are out numbered by the devils on either side of the river, they get killed and the game is over." +" You can try it in many ways. Keep all priests alive! Good luck!");}
}
在FirstController类中,只需加一个tips()函数。
public ArrayList tips(){int pristNumber = 0;int devilNumber = 0;//获取右边对应的人数int[] fromCount = fromLand.getChaNum();pristNumber += fromCount[0];devilNumber += fromCount[1];//将船上人的数目也加上去int[] boatCount = boat.getChaNum();if (boat.getOnWhere() == 1){pristNumber += boatCount[0];devilNumber += boatCount[1];}return ai.findPath(pristNumber, devilNumber, boat.getOnWhere());}
牧师与恶魔过河游戏——智能提示相关推荐
- 3D游戏编程与设计 PD(牧师与恶魔)过河游戏智能帮助实现
3D游戏编程与设计 P&D(牧师与恶魔) 过河游戏智能帮助实现 文章目录 3D游戏编程与设计 P&D(牧师与恶魔) 过河游戏智能帮助实现 一.作业与练习 二.设计简述 1. 状态图基础 ...
- 【Unity3d学习】魔鬼与牧师过河游戏智能帮助
文章目录 写在前面 实验内容 状态图自动生成(使用DFS) 1. 状态表示 2.DFS算法实现 3.DFS生成结果 更改Controller 效果展示 写在前面 本次项目Github地址:传送门 本次 ...
- Unity3d入门之路-PD 过河游戏智能帮助
文章目录 P&D 过河游戏智能帮助 状态图 实现方法 图的表示方法 广度优先搜索 P&D 过河游戏拓展 结果展示 P&D 过河游戏智能帮助 本次作业基本要求是三选一,我选择了P ...
- Unity学习之PD 过河游戏智能帮助实现
Unity学习之P&D 过河游戏智能帮助实现 根据之前设计好的动作分离版过河游戏,我们进行一个简单的状态图AI实现. 转移状态图 状态图老师已经给出: 该状态图只记录了游戏过程中左岸的情况.P ...
- 【Unity 3D学习笔记】PD 过河游戏智能实现
P&D 过河游戏智能帮助实现 实现状态图的自动生成 讲解图数据在程序中的表示方法 利用算法实现下一步的计算 对于过河游戏,首先需要知道其中各个状态之间的转换关系,绘制状态转移图如下: 其中,P ...
- unity:PD 过河游戏智能帮助实现
P&D 过河游戏智能帮助实现 github传送门 状态图 状态图课件有 状态图(Statechart Diagram)是描述一个实体基于事件反应的动态行为,显示了该实体如何根据当前所处的状态对 ...
- 3D游戏编程实践——PD 过河游戏智能帮助实现
P&D 过河游戏智能帮助实现 需求 实现状态图的自动生成 讲解图数据在程序中的表示方法 利用算法实现下一步的计算 实现过程 实现状态图的自动生成&讲解图数据在程序中的表示方法 牧师与魔 ...
- PD 过河游戏智能帮助实现
github传送门 https://github.com/ddghost/unity3d_n/tree/R%26D%E6%99%BA%E8%83%BD%E5%B8%AE%E5%8A%A9%E6%8F% ...
- 3D-太阳系、牧师与恶魔小游戏
1.简单并用程序验证 游戏对象运动的本质是什么? 本质上是游戏对象坐标的变换 使用三种方法实现物体的抛物线运动 Vector3.MoveTowards private float step = Tim ...
最新文章
- Docker for mac安装
- python的数字比较好_说说 Python3 中的数字处理
- IOS 中runtime 不可变数组__NSArray0 和__NSArrayI
- redis live 如何安装
- [LeetCode] Longest Consecutive Sequence 求解
- Bose 700无线消噪耳机评测:让用户不受打扰是它最大的温柔
- python和c先学哪个-python和c先学哪个
- python中文字体奇怪_利用python检查 AS400的中文字问题
- Nginx源码分析 - 主流程篇 - 解析配置文件(13)
- Git之路——配置SSH免密码登录
- 1.3 收敛数列的性质
- win10 SystemParametersInfo 设置屏保 不好使_抖音网红屏保时钟软件(附使用教程)...
- OHS简单安装与系统配置
- Eclipse执行junit测试时出现Errors occurred during the build. Errors running builder 'Integrated External Too
- 麦咖啡导致电脑不能上网
- 全国天气预报api接口
- mac打开chm文件
- php开发的app商城,如何利用PHP语言开发手机APP
- 分布式服务器时间同步
- Android 7.0 GMS测试 Camera模块CTS fail项分析