unity3D学习9 游戏智能

游戏智能


作业要求

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

实现过程

首先我们对这个过河问题进行一下简单的分析。游戏中的每一个状态可以由当前左右两边各有多少个恶魔和牧师,以及现在船在哪一边来唯一的标识。而且在保证每一边恶魔数量不多于牧师,船上人数大于0小于等于2的情况下,不同状态之间存在相互转移关系。所以整个游戏的运行过程可以表示为一个图的形式,而且这个图是无向有环的,因为两个相连的状态必然能够相互转换,而且经过一段时间的变换可能会回到之前的状态。那么整个问题就变为在状态转移图上寻找一条从起始状态(3个恶魔和3个牧师都在右边)到结束状态(3个恶魔和3个牧师都在左边)的路径。所以整个算法实际上就是一个图搜索算法,这里我们可以使用深度优先搜索。
为了方便调试,我首先写了一个C++的模拟程序。这里使用一个三元组(ld, lp, side)ld:左边恶魔数、lp:左边牧师数、side:船在哪一边,这三个信息唯一的标识一个状态。使用一个visited数组记录当前是否展开过某个状态,这样可以防止搜索过程中重复展开已经展开过的状态。使用一个search函数递归的进行搜索,然后返回一个记录了可行路径的栈。详细程序如下:

#include <bits/stdc++.h>
using namespace std;

bool visited[4][4][2]; //标志一个状态是否被展开过 

bool is_valid(int ld, int lp){ //判断一个状态是否违反规则(某一边恶魔大于牧师) 
	if((lp!=0 && ld > lp) || ((3-lp!=0) && (3-ld)> (3-lp)))return false;
	return true;
}

stack<pair<int, int> > search(int ld, int lp, int side){ //输入一个状态,进行寻路 
	visited[ld][lp][side] = true;
	stack<pair<int, int> > path;
	if(ld==3 && lp==3){  //结束状态,用全0标记 
		path.push(pair<int, int>(0,0));
		return path;
	}
	int D = (side==0)?ld:(3-ld);
	int P = (side==0)?lp:(3-lp);
	int next_side = (side==0)?1:0;
	for(int d = 0; d <= D; d++){
		for(int p = 0; p <= P; p++){
			if(d+p>2 || d+p==0)continue;
			int temp_ld = (side==0)?ld-d:ld+d;
			int temp_lp = (side==0)?lp-p:lp+p;
			if(!is_valid(temp_ld, temp_lp))continue; //检查变换之后是否违反规则 
			if(visited[temp_ld][temp_lp][next_side])continue; //检测变换之后的状态是否已经展开过 
			printf("(%d, %d, %d)->(%d, %d, %d)\n",ld,lp,side,temp_ld,temp_lp,next_side);
			path = search(temp_ld, temp_lp, next_side); //递归查找一条路径 
			if(path.size()!=0){ //如果返回结果为空,说明是死路 
				path.push(pair<int,int>(d,p));
				return path;
			}
		}
	}
	return path;
}

int main(){
	memset(visited, false, sizeof(visited));
	stack<pair<int, int> > path;
	path = search(0,0,1); //左边没有Devil和Priest,船在右边作为起始状态 
	int curr_side = 1;
	while(path.size()!=0){
		pair<int, int> move = path.top();
		path.pop();
		curr_side = (curr_side==1)?0:1;
		if(move.first==0 && move.second==0){
			cout<<"success!"<<endl; 
			break;
		}
		printf("move %d Devils and %d Priests ", move.first,move.second);
		string move_str = (curr_side==0)?"from right to left":"from left to right";
		cout<<move_str<<endl;
	}
} 

程序运行结果

程序运行之后显示了从(0,0,1)到(3,3,0)状态,也就是从起始状态到结束状态的搜索路径。最后输出了一系列的移动策略。
在这里插入图片描述

状态转化图

将上面程序输出的状态转移信息放到一个状态转换图中,就得到了以下结果:
在这里插入图片描述
上图并不是完整的状态转移关系,准确来说只是深度优先搜索过程中的状态转移关系。这个状态图显示了一条从起始状态到结束状态的路径,以及搜索过程中展开的一些死路。在展开过程中我没有展开那些违反规则(GameOver)的状态,也不会展开那些曾经展开过的状态,所以整个状态转移图就非常简单。
当然,开始状态不一定是(0,0,1),我们可以从任何合法的状态起步。由于整个状态转移图是全联通的,所以我们必然能够找到一个到达结束状态的路径。(这里可以简单证明一下,我们选择的开始状态必然是从(0,0,1)转换来的,所以存在到达(0,0,1)的路径,而(0,0,1)存在到达(3,3,0)的路径,所以这个问题一定有解)

实现代码

接下来的任务就是将这一段代码结合到之前的代码中,从而实现自动运行的功能。C#中的库和C++有些区别,我使用C#的Stack代替C++的stack,C#的KeyValuePair代替C++的pair。为了使整个移动效果依次进行,而不是计算出了路径一下就运行完了,我在update函数中每隔一段时间调用一下nextstep,然后nextstep会从栈顶得到当前路径并进行移动,其他部分的实现大体没变。

public class Controller : MonoBehaviour, ISceneController, IUserAction
{
    private GameObject left_land, right_land, river;
    private CharacterModel[] MCharacter;  
    private BoatModel MBoat;
    private bool[,,] visited;
    public CCActionManager actionManager;
    public CCJudgement judgement;
    Stack<KeyValuePair<int, int>> path;
    bool auto_mode;
    float spendTime;
    // Start is called before the first frame update
    void Awake()
    {
        SSDirector director = SSDirector.getInstance();
        director.currentSceneController = this;
        MCharacter = new CharacterModel[6];
        director.currentSceneController.LoadResources();
        actionManager = gameObject.AddComponent<CCActionManager>() as CCActionManager;
        visited = new bool[4,4,2];
        auto_mode = false;
    }

    public void LoadResources()
    {
        Vector3 left_land_pos = new Vector3(-8F, 0, 0);
        Vector3 right_land_pos = new Vector3(8F, 0, 0);
        Vector3 river_pos = new Vector3(0, -0.5F, 0);
        Vector3 boat_pos = new Vector3(4F, 0.25F, 0);
        Vector3[] charactor_pos = { new Vector3(5.25F, 1.25F, 0), new Vector3(6.25F, 1.25F, 0), new Vector3(7.25F, 1.25F, 0),
                                    new Vector3(8.25F, 1.25F, 0), new Vector3(9.25F, 1.25F, 0), new Vector3(10.25F, 1.25F, 0) };
        left_land = Object.Instantiate(Resources.Load("Land", typeof(GameObject)), left_land_pos, Quaternion.identity, null) as GameObject;
        left_land.name = "left_land";
        right_land = Object.Instantiate(Resources.Load("Land", typeof(GameObject)), right_land_pos, Quaternion.identity, null) as GameObject;
        right_land.name = "right_land";
        river = Object.Instantiate(Resources.Load("River", typeof(GameObject)), river_pos, Quaternion.identity, null) as GameObject;
        river.name = "river";

        MBoat = new BoatModel(boat_pos);

        for (int i = 0; i < 6; i++)
        {
            if (i < 3)
                MCharacter[i] = new CharacterModel(0, charactor_pos[i], i);
            else
                MCharacter[i] = new CharacterModel(1, charactor_pos[i], i);
        }
    }

    public int get_character_side(int num)
    {
        return MCharacter[num].get_side();
    }

    public void change_game_situation()
    {
        UserGUI.situation = judgement.GetSituation();
        if (UserGUI.situation != 0) stop_all();
    }

    public void stop_all()
    {
        for (int i = 0; i < 6; i++)
            MCharacter[i].stop_character();
        MBoat.stop_boat();
    }
    public void enable_all()
    {
        for (int i = 0; i < 6; i++)
            MCharacter[i].enable_character();
        MBoat.enable_boat();
    }

    bool is_valid(int ld, int lp)
    { //判断一个状态是否违反规则(某一边恶魔大于牧师) 
        if ((lp != 0 && ld > lp) || ((3 - lp != 0) && (3 - ld) > (3 - lp))) return false;
        return true;
    }


    Stack<KeyValuePair<int, int> > search(int ld, int lp, int side)
    { //输入一个状态,进行寻路 
        visited[ld,lp,side] = true;
        Stack<KeyValuePair<int, int> > path = new Stack<KeyValuePair<int, int>>();
        if (ld == 3 && lp == 3)
        {  //结束状态,用全0标记 
            path.Push(new KeyValuePair<int, int>(0, 0));
            return path;
        }
        int D = (side == 0) ? ld : (3 - ld);
        int P = (side == 0) ? lp : (3 - lp);
        int next_side = (side == 0) ? 1 : 0;
        for (int d = 0; d <= D; d++)
        {
            for (int p = 0; p <= P; p++)
            {
                if (d + p > 2 || d + p == 0) continue;
                int temp_ld = (side == 0) ? ld - d : ld + d;
                int temp_lp = (side == 0) ? lp - p : lp + p;
                if (!is_valid(temp_ld, temp_lp)) continue; //检查变换之后是否违反规则 
                if (visited[temp_ld,temp_lp,next_side]) continue; //检测变换之后的状态是否已经展开过 
                //Debug.Log("(%d, %d, %d)->(%d, %d, %d)\n", ld, lp, side, temp_ld, temp_lp, next_side);
                path = search(temp_ld, temp_lp, next_side); //递归查找一条路径 
                if (path.Count != 0)
                { //如果返回结果为空,说明是死路 
                    path.Push(new KeyValuePair<int, int>(d, p));
                    return path;
                }
            }
        }
        return path;
    }

    public void auto_move()
    {
        for (int i = 0; i < 4; i++)
            for (int j = 0; j < 4; j++)
                for (int k = 0; k < 2; k++)
                    visited[i, j, k] = false;
        int left_d = 0, left_p = 0, right_d = 0, right_p = 0;
        for (int i = 0; i < 6; i++)
        {
            if (i < 3)
            {
                if (get_character_side(i) == -1)
                    right_d += 1;
                else
                    left_d += 1;
            }
            else
            {
                if (get_character_side(i) == -1)
                    right_p += 1;
                else
                    left_p += 1;
            }
        }
        int side = (MBoat.get_side()==-1)?1:0;
        path = search(left_d, left_p, side);
        auto_mode = true;
        this.stop_all();
    } 

    void next_step()
    {
        if (path.Count == 0) return;
        int side = MBoat.get_side();
        KeyValuePair<int, int> move = path.Pop();
        if (move.Key == 0 && move.Value == 0)
        {
            Debug.Log("success");
            return;
        }
        string output = "move " + (move.Key).ToString() + " Devils and " + (move.Value).ToString() + " Priests ";
        string move_str = (side == 0) ? "from right to left" : "from left to right";
        output += move_str;
        Debug.Log(output);
        for(int i = 0; i < 6; i++)
        {
            if (MCharacter[i].get_whether_on_boat())
            {
                to_land(i);
            }
        }
        int curr_d = 0, curr_p = 0;
        for(int i = 0; i < 6; i++)
        {
            if(MCharacter[i].get_side() == side)
            {
                if(i < 3 && curr_d < move.Key)
                {
                    take_boat(i);
                    curr_d+=1;
                }
                if(i >= 3 && curr_p < move.Value)
                {
                    take_boat(i);
                    curr_p += 1;
                }
            }
        }
        move_boat();
    }

    public void move_boat()
    {
        Debug.Log("Move boat");
        if (MBoat.is_empty())
            return;
        MBoat.turn_side();
        int[] custom_num = MBoat.get_customs();
        if (custom_num[0] != -1)
        {
            MCharacter[custom_num[0]].turn_side();
            actionManager.Move(MCharacter[custom_num[0]].character, MCharacter[custom_num[0]].get_dst(), 20);
        }
        if (custom_num[1] != -1)
        {
            MCharacter[custom_num[1]].turn_side();
            actionManager.Move(MCharacter[custom_num[1]].character, MCharacter[custom_num[1]].get_dst(), 20);
        }
        actionManager.Move(MBoat.boat, MBoat.get_dst(), 20);
        this.stop_all();
        Debug.Log(UserGUI.situation);
    }

    public void click_character(int character_num)
    {
        if (MCharacter[character_num].get_whether_on_boat())
            to_land(character_num);
        else
            take_boat(character_num);
    }

    public void take_boat(int character_num)
    {
        if (!MBoat.has_empty() || MCharacter[character_num].get_side() != MBoat.get_side()||
             MCharacter[character_num].get_whether_on_boat())
            return;
        Vector3 boat_seat = MBoat.get_seat(character_num);
        MCharacter[character_num].take_boat(boat_seat);
    }

    public void to_land(int character_num)
    {
        if (!MCharacter[character_num].get_whether_on_boat())
            return;
        MBoat.clear_seat(character_num);
        MCharacter[character_num].to_land();
    }

    // Update is called once per frame
    void Update()
    {
        if (!auto_mode) return;
        spendTime += Time.deltaTime;
        if(spendTime > 1.5)
        {
            spendTime = 0;
            next_step();
        }
    }

    public void restart()
    {
        MBoat.restart();
        auto_mode = false;
        for (int i = 0; i < 6; i++)
            MCharacter[i].restart();
        enable_all();
    }
}

运行效果

在这里插入图片描述
点击Auto Move按钮之后,程序就会根据当前状态计算出一条可以达到最终状态的运动路径,并自动控制角色的上下船和船的左右移动。
完整工程文件请查看我的Github,如果有什么问题请及时指出,谢谢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值