本文利用图的宽度搜索实现牧师与恶魔的AI提示功能并且实现了状态图的自动生成。
基础的牧师与恶魔游戏功能,请参照起初的几篇博客,这里就不再重复叙述,而主要针对AI部分的分析
游戏演示视频地址:http://www.iqiyi.com/w_19rz09ze4h.html
游戏具体代码地址:https://github.com/dick20/3d-learning/tree/master/homework9
一. 游戏要求
P&D 过河游戏智能帮助实现,程序具体要求:
- 实现状态图的自动生成
- 讲解图数据在程序中的表示方法
- 利用算法实现下一步的计算
二. 设计过程
A.实现状态图的自动生成
1.基础知识
Q: 什么是状态图?
A: 状态图(Statechart Diagram)是描述一个实体基于事件反应的动态行为,显示了该实体如何根据当前所处的状态对不同的事件做出反应。
Q: 牧师与恶魔游戏的状态图有哪些?
A:
左岸牧师 | 左岸恶魔 | 右岸牧师 | 右岸恶魔 | 船只位置 |
---|---|---|---|---|
3 | 3 | 0 | 0 | left |
0 | 0 | 3 | 3 | right |
2 | 2 | 1 | 1 | right |
… | … | … | … | … |
一共有3*3*2-1 = 17个状态,这里不一一列出,而是通过自动生成。
Q: 为什么要自动实现牧师与恶魔游戏的状态图?
A: 由于这个游戏仅需要3个牧师,3个恶魔,一个船只,两个岸边,我们可以通过枚举的形式来写出多个状态图以及它们的联系。这样看起来是可行的,但是这样的复用性的就很差,而且很不程序员,所以我们需要自动生成状态图,而不是枚举计算。
2.设计步骤
我将游戏状态封装成一个类,其中包括5个基础元素,左岸牧师数,左岸恶魔数,右岸牧师数,右岸恶魔数,船的位置。然后增加一个父节点来构建图,可以通过当前的状态找到前一个状态。
在此,当游戏不断进行时,我们通过UserAction接口中可以得到,左岸的角色数目,右岸的角色数目,船的位置等等,可以通过这些信息就能实现一个状态。
public class PristsAndDevilsState{
public int leftPriests;
public int rightPriests;
public int leftDevils;
public int rightDevils;
public bool boat_pos; //true表示船在左边,false表示船在右边
public PristsAndDevilsState parent_state;
//缺省构造函数
public PristsAndDevilsState() { }
//带参构造函数
public PristsAndDevilsState(int leftPriests, int leftDevils, int rightPriests,
int rightDevils, bool boat_pos, PristsAndDevilsState parent_state)
{
this.leftPriests = leftPriests;
this.rightPriests = rightPriests;
this.leftDevils = leftDevils;
this.rightDevils = rightDevils;
this.boat_pos = boat_pos;
this.parent_state = parent_state;
}
//复制构造函数
public PristsAndDevilsState(PristsAndDevilsState temp)
{
this.leftPriests = temp.leftPriests;
this.rightPriests = temp.rightPriests;
this.leftDevils = temp.leftDevils;
this.rightDevils = temp.rightDevils;
this.boat_pos = temp.boat_pos;
this.parent_state = temp.parent_state;
}
```
}
B.讲解图数据在程序中的表示方法
1.基础知识
Q: 图数据在程序中的表示方法有哪些?
A: 邻接矩阵法,对于一个有n个点的图,它需要一个n*n的矩阵,这个矩阵的第i行第j列的数值表示点vi到vj的距离。
邻接表法,邻接表是图的一种链式存储结构。对于图G中每个顶点vi,把所有邻接于vi的顶点vj链成一个单链表,这个单链表称为顶点vi的邻接表。
Q: 我采取了怎样的表示方法
A: 为了后面使用宽度搜索算法的方便性,我采用的是邻接表法来表示这个图。
2.设计步骤
将每一个访问状态结点放入队列,而每一个结点都可以找到前面一个的父亲结点,故到最后可以找到一条路径到达目标状态。
public static PristsAndDevilsState BFS(PristsAndDevilsState start, PristsAndDevilsState end)
{
//存放到达目的的路径状态
Queue<PristsAndDevilsState> found = new Queue<PristsAndDevilsState>();
PristsAndDevilsState temp = new PristsAndDevilsState(start.leftPriests, start.leftDevils, start.rightPriests, start.rightDevils, start.boat_pos, null);
//当前状态入队
found.Enqueue(temp);
//队列元素数目大于0
while (found.Count > 0)
{
temp = found.Peek();
//当状态等于末状态时,可以终止继续寻找,只需通过parent_state来找出开始状态的下一个状态即可
//当然也可以把整个路径显示出来
if (temp == end)
{
while (temp.parent_state != start)
{
temp = temp.parent_state;
}
return temp;
}
```
}
C.利用宽度搜索算法实现下一步的计算
1.基础知识
Q: 什么是宽度搜索算法?
A: 宽度优先搜索的核心思想是:从初始结点开始,应用算符生成第一层结点,检查目标结点是否在这些后继结点中,若没有,再用产生式规则将所有第一层的结点逐一扩展,得到第二层结点,并逐一检查第二层结点中是否包含目标结点。若没有,再用算符逐一扩展第二层所有结点……,如此依次扩展,直到发现目标结点为止 。
Q: 这个状态图如何使用宽度搜索算法有哪些?
A: 解决这个问题就需要考虑搜索的判断,由于游戏规则的限制,我们的搜索可分为这两种大情况,船在左边,船在右边。而每一种大情况由可以细分为,船可搭载一个牧师,船可搭载一个恶魔,船可搭载一个牧师和一个恶魔,船可搭载两个牧师,船可搭载两个恶魔这五种情况。
所以,我们在宽度搜索算法从四个方向进行宽度搜索改为上述的十种情况的搜索。
2.设计步骤
宽度搜索算法伪代码如下:
1 . 初始化队列Q;
2. 访问顶点v; visited [v]=1; 顶点v入队Q;
3. while (队列Q非空)
3.1 v=队列Q的队头元素出队;
3.2 w=顶点v的第一个邻接点;
3.3 while (w存在)
3.3.1 如果w 未被访问,则
访问顶点w; visited[w]=1; 顶点w入队列Q;
3.3.2 w=顶点v的下一个邻接点;
根据伪代码来编写宽度搜索算法,其中要注意每种情况的数目增减情况,船的移动的移动情况。
public static PristsAndDevilsState BFS(PristsAndDevilsState start, PristsAndDevilsState end)
{
//存放到达目的的路径状态
Queue<PristsAndDevilsState> found = new Queue<PristsAndDevilsState>();
PristsAndDevilsState temp = new PristsAndDevilsState(start.leftPriests, start.leftDevils, start.rightPriests, start.rightDevils, start.boat_pos, null);
//当前状态入队
found.Enqueue(temp);
//队列元素数目大于0
while (found.Count > 0)
{
temp = found.Peek();
//当状态等于末状态时,可以终止继续寻找,只需通过parent_state来找出开始状态的下一个状态即可
//当然也可以把整个路径显示出来
if (temp == end)
{
while (temp.parent_state != start)
{
temp = temp.parent_state;
}
return temp;
}
found.Dequeue();
// 判断该状态的船的位置,船若在左边
if (temp.boat_pos)
{
// 将一个牧师移动到右边
if (temp.leftPriests > 0)
{
PristsAndDevilsState next = new PristsAndDevilsState(temp);
next.parent_state = new PristsAndDevilsState(temp);
next.boat_pos = false;
next.leftPriests--;
next.rightPriests++;
if (next.isValid() && !found.Contains(next))
{
found.Enqueue(next);
}
}
// 将一个恶魔移动到右边
if (temp.leftDevils > 0)
{
PristsAndDevilsState next = new PristsAndDevilsState(temp);
next.parent_state = new PristsAndDevilsState(temp);
next.boat_pos = false;
next.leftDevils--;
next.rightDevils++;
if (next.isValid() && !found.Contains(next))
{
found.Enqueue(next);
}
}
// 将一个牧师以及一个恶魔移动到右边
if (temp.leftDevils > 0 && temp.leftPriests > 0)
{
PristsAndDevilsState next = new PristsAndDevilsState(temp);
next.parent_state = new PristsAndDevilsState(temp);
next.boat_pos = false;
next.leftDevils--;
next.rightDevils++;
next.leftPriests--;
next.rightPriests++;
if (next.isValid() && !found.Contains(next))
{
found.Enqueue(next);
}
}
// 将两个牧师移动到右边
if (temp.leftPriests > 1)
{
PristsAndDevilsState next = new PristsAndDevilsState(temp);
next.parent_state = new PristsAndDevilsState(temp);
next.boat_pos = false;
next.leftPriests -= 2;
next.rightPriests += 2;
if (next.isValid() && !found.Contains(next))
{
found.Enqueue(next);
}
}
// 将两个恶魔移动到右边
if (temp.leftDevils > 1)
{
PristsAndDevilsState next = new PristsAndDevilsState(temp);
next.parent_state = new PristsAndDevilsState(temp);
next.boat_pos = false;
next.leftDevils -= 2;
next.rightDevils += 2;
if (next.isValid() && !found.Contains(next))
{
found.Enqueue(next);
}
}
}
// 判断该状态的船的位置,船若在右边
else
{
// 将一个牧师移动到左边
if (temp.rightPriests > 0)
{
PristsAndDevilsState next = new PristsAndDevilsState(temp);
next.parent_state = new PristsAndDevilsState(temp);
next.boat_pos = true;
next.rightPriests--;
next.leftPriests++;
if (next.isValid() && !found.Contains(next))
{
found.Enqueue(next);
}
}
// 将一个恶魔移动到右边
if (temp.rightDevils > 0)
{
PristsAndDevilsState next = new PristsAndDevilsState(temp);
next.parent_state = new PristsAndDevilsState(temp);
next.boat_pos = true;
next.rightDevils--;
next.leftDevils++;
if (next.isValid() && !found.Contains(next))
{
found.Enqueue(next);
}
}
// 将一个牧师一个恶魔移动到右边
if (temp.rightDevils > 0 && temp.rightPriests > 0)
{
PristsAndDevilsState next = new PristsAndDevilsState(temp);
next.parent_state = new PristsAndDevilsState(temp);
next.boat_pos = true;
next.rightDevils--;
next.leftDevils++;
next.rightPriests--;
next.leftPriests++;
if (next.isValid() && !found.Contains(next))
{
found.Enqueue(next);
}
}
// 将两个恶魔移动到右边
if (temp.rightDevils > 1)
{
PristsAndDevilsState next = new PristsAndDevilsState(temp);
next.parent_state = new PristsAndDevilsState(temp);
next.boat_pos = true;
next.rightDevils -= 2;
next.leftDevils += 2;
if (next.isValid() && !found.Contains(next))
{
found.Enqueue(next);
}
}
// 将两个牧师移动到右边
if (temp.rightPriests > 1)
{
PristsAndDevilsState next = new PristsAndDevilsState(temp);
next.parent_state = new PristsAndDevilsState(temp);
next.boat_pos = true;
next.rightPriests -= 2;
next.leftPriests += 2;
if (next.isValid() && !found.Contains(next))
{
found.Enqueue(next);
}
}
}
}
return null;
}
D.利用上述实现的函数来与UI交互
这里,就是改动我们之前的类,提供信息给这个宽度函数函数以及在OnGUI中显示提示界面。
以下是新增加的部分,主要是在按键后,通过调用action接口拿取角色数目来形成当前状态,然后将这个状态作为参数传入BFS函数中,返回下一个状态,将该状态显示在界面即可
if (GUI.Button(new Rect(Screen.width / 2 - 170, 20, 100, 50), "Tips", buttonStyle))
{
int[] arr = action.getNum();
leftPriests = arr[0];
leftDevils = arr[1];
rightPriests = arr[2];
rightDevils = arr[3];
Debug.Log(arr[4]);
if (arr[4] == 0) { boat_pos = true; }
else { boat_pos = false; }
start = new PristsAndDevilsState(leftPriests, leftDevils, rightPriests, rightDevils, boat_pos, null);
Debug.Log(start.leftPriests+ " " + start.leftDevils + " " + start.rightPriests + " " + start.rightDevils + " " + start.boat_pos);
Debug.Log(end.leftPriests + " " + end.leftDevils + " " + end.rightPriests + " " + end.rightDevils + " " + end.boat_pos);
//bug
PristsAndDevilsState temp = PristsAndDevilsState.BFS(start, end);
leftPriests = temp.leftPriests;
leftDevils = temp.leftDevils;
rightPriests = temp.rightPriests;
rightDevils = temp.rightDevils;
tips = "try to make\nleftPriests : " + leftPriests + "\nleftDevils : " + leftDevils
+ "\nrightPriests : " + rightPriests + "\nrightDevils : " + rightDevils;
}
if (GUI.Button(new Rect(Screen.width / 2 - 170, 80, 100, 50), "Hide Tips", buttonStyle))
{
tips = "";
}
游戏实现效果图如下:
三. 总结
完成这个牧师与恶魔的第三次改动,我最大的感受是,前面的动作分离以及MVC模式给了我后面的修改极大的方便,如果前面的模式没有分离,那么后面的改动将会无法下手。然后,对于AI部分,我学到了通过一些算法来得到游戏提示,当然这是极为简单游戏规则限定。如何利用好状态图,这也是实现AI的一大关键。
由于作者水平有限,如有任何错误请指出并讨论,十分感谢!
想了解更多关于3d游戏设计代码,可以点击我的Github一起学习。
Github地址:https://github.com/dick20/3d-learning