P&D 过河游戏智能帮助实现,程序具体要求:
- 实现状态图的自动生成
- 讲解图数据在程序中的表示方法
- 利用算法实现下一步的计算
游戏状态图
参考博客中的游戏状态转化图,用“左岸牧师数、左岸恶魔数、船的位置(左/右)”
即可以完整地表示游戏所处的状态。
- 注意,我们不关心此刻牧师/恶魔在船上还是在岸上,船靠左,则船上的人都记为左岸;船靠右,则船上的人都记为右岸
如下图所示,箭头表示当处于这个状态时,需要转移到的下一个状态。这样的转移需要逐步接近成功状态,而避免进入错误状态。
代码实现
获取下一个状态
编写一个ai文件,实现一个函数以获取下一状态
-
参数:输入当前状态
“左岸牧师数、左岸恶魔数、船的位置(左/右)”
-
返回:根据状态转移图,获得下一状态。返回值为,从船所在的岸 转移到 另一岸 的牧师数量和恶魔数量,用next[2]记录。
- 例如下图的这个状态转移过程,需要转移的牧师数量为1,需要转移的恶魔数量为1
- 不需要记录船是从左到右还是从右到左,因为每次状态转移都包含了船的一次转移过程:船载着人到达另一岸
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace myGame {
public class AIcontroller : MonoBehaviour {
public int[] getNextAction(int curNumDevilLeft, int curNumPriestLeft, int curBoatPos)
{
//next[0] 从 岸 转移到 另一岸 的牧师数
//next[1] 从 岸 转移到 另一岸 的恶魔数
int[] next = new int[2];
int P = curNumPriestLeft, D = curNumDevilLeft, B = curBoatPos; //1:左边 2:右边
if (P == 3 && D == 3 && B == 1)
{
next[0] = 1;
next[1] = 1;
}
else if(P == 3 && D == 2 && B == 2)
{
next[0] = 0;
next[1] = 1;
}
else if(P == 2 && D == 2 && B == 2)
{
next[0] = 1;
next[1] = 0;
}
else if(P == 3 && D == 1 && B == 2)
{
next[0] = 0;
next[1] = 1;
}
else if(P == 3 && D == 2 && B == 1)
{
next[0] = 0;
next[1] = 2;
}
else if(P == 3 && D == 0 && B == 2)
{
next[0] = 0;
next[1] = 1;
}
else if(P == 3 && D == 1 && B == 1)
{
next[0] = 2;
next[1] = 0;
}
else if(P == 1 && D == 1 && B == 2)
{
next[0] = 1;
next[1] = 1;
}
else if(P == 2 && D == 2 && B == 1)
{
next[0] = 2;
next[1] = 0;
}
else if(P == 0 && D == 2 && B == 2)
{
next[0] = 0;
next[1] = 1;
}
else if(P == 0 && D == 3 && B == 1)
{
next[0] = 0;
next[1] = 2;
}
else if(P == 0 && D == 1 && B == 2)
{
next[0] = 0;
next[1] = 1;
}
else if(P == 2 && D == 1 && B == 1)
{
next[0] = 2;
next[1] = 0;
}
else if(P == 1 && D == 1 && B == 1)
{
next[0] = 1;
next[1] = 1;
}
else if(P == 0 && D == 2 && B == 1)
{
next[0] = 0;
next[1] = 2;
}
return next;
}
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
}
}
FirstController
在FirstController
中需要处理的任务如下:
获取当前状态
统计在左边的牧师/恶魔数量和船的位置。
//得到当前状态
//计算所有在左边的牧师/恶魔
int numDevilLeft = leftBank.getNumDevil(), numPriestLeft = leftBank.getNumPriest();
int numDevilBoat = boat.getNumDevil(), numPriestBoat = boat.getNumPriest();
if (boat.getState() == 1)
{
numDevilLeft += numDevilBoat;
numPriestLeft += numPriestBoat;
}
Debug.Log("numDevilLeft " + numDevilLeft);
Debug.Log("numPriestLeft " + numPriestLeft);
int boatPos = boat.getState();
获取下一状态
通过调用ai得到需要转移的牧师/恶魔数
//获取下一个状态
int[] next = ai.getNextAction(numDevilLeft, numPriestLeft, boat.getState());
Debug.Log("next[0] " + next[0]);
Debug.Log("next[1] " + next[1]);
移动牧师/恶魔
需要转移的牧师/恶魔,就是要放在船上的人。但是由于当前玩家的动作,使得船上可能已经有了一些牧师/恶魔。
此时想要使“点击”次数最少,遵循以下逻辑:
numDveilToBoat = 0, numPriestToBoat = 0 表示已经完成的在船上的人数,目标是和next一样
先boat -> 岸
第一个是P
next[0] > 0 留着 numPriestToBoat++
next[0] = 0 放回去
第一个是D
next[1] 同上 numDveilToBoat++
第二个是P
next[0] = 2 且 numPriestToBoat = 1 留着
next[0] = 1 放回去
第二个是D
同上
再 岸 -> boat
看一下 num和next没满足的,放岸上的去船上即可
移动的过程只要调用之前实现好的clickACharacter
函数即可。
int numPriestToBoat = 0, numDevilToBoat = 0;
int[] personOnBoat = boat.getPersonOnBoat();
if (personOnBoat[0] >= 3 && personOnBoat[0] <= 6) //第一个是P
{
if (next[0] > 0) //P保持不动
numPriestToBoat++;
else if(next[0] == 0) //把这个P放去岸上
{
clickACharacter(characters[personOnBoat[0]]);
}
}
else if(personOnBoat[0] >= 0 && personOnBoat[0] <= 2) //第一个是D
{
if (next[1] > 0)
numDevilToBoat++;
else if(next[1] == 0)
{
clickACharacter(characters[personOnBoat[0]]);
}
}
if (personOnBoat[1] >= 3 && personOnBoat[1] <= 6) //第二个是P
{
if (next[0] == 2 || (next[0] == 1 && numPriestToBoat == 0)) //P不用动
numPriestToBoat++;
else if (next[0] == 0 || (next[0] == 1 && numPriestToBoat == 1)) //把这个P放去岸上
{
clickACharacter(characters[personOnBoat[1]]);
}
}
else if (personOnBoat[1] >= 0 && personOnBoat[1] <= 2) //第二个是D
{
if (next[1] == 2 || (next[1] == 1 && numDevilToBoat == 0))
numDevilToBoat++;
else if(next[1] == 0 || (next[1] == 1 && numDevilToBoat == 1))
{
clickACharacter(characters[personOnBoat[1]]);
}
}
/*
岸 -> 船
*/
for (int i = 0; i < next[0] - numPriestToBoat; i++) // P
{
int index = -1;
if (boatPos == 1)
index = leftBank.getAPriestIndex();
else if (boatPos == 2)
index = rightBank.getAPriestIndex();
clickACharacter(characters[index]);
}
for(int i = 0; i < next[1] - numDevilToBoat; i++) // P
{
int index = -1;
if (boatPos == 1)
index = leftBank.getADevilIndex();
else if (boatPos == 2)
index = rightBank.getADevilIndex();
clickACharacter(characters[index]);
}
调用的小函数再在 BoatController 和 CharacterController 中实现。
移动船
由于移动Character的过程是运动的,需要花费一定时间完成。如果马上调用moveBoat函数,则角色还没有到船上就过河了。
使用延迟函数Invoke
,在延迟0.6s后再调用moveBoat
函数
//移动船
Invoke("moveBoat", (float)0.6);
增加按钮
增加一个"NextAction"按钮。
在接口中增加函数
//玩家与游戏交互的接口(玩家的所有操作)
public interface IUserAction
{
void moveBoat();
void clickACharacter(CharactersController character);
void restart();
void NextActionAI();
}
userGUI在页面上显示按钮
if (GUI.Button(new Rect(Screen.width / 2, 240, 170, 30), "Tips:NextAction"))
{
action.NextActionAI();
}