unity智能巡逻兵

 

 

 

智能巡逻兵

  • 提交要求:
  • 游戏设计要求:
    • 创建一个地图和若干巡逻兵(使用动画);
    • 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
    • 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
    • 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
    • 失去玩家目标后,继续巡逻;
    • 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
  • 程序设计要求:
    • 必须使用订阅与发布模式传消息
      • subject:OnLostGoal
      • Publisher: ?
      • Subscriber: ?
    • 工厂模式生产巡逻兵
  • 友善提示1:生成 3~5个边的凸多边型
    • 随机生成矩形
    • 在矩形每个边上随机找点,可得到 3 - 4 的凸多边型
    • 5 ?
  • 友善提示2:参考以前博客,给出自己新玩法

游戏实现

巡逻兵部分

巡逻兵部分实现了进行矩形路线的自动巡逻移动,当玩家进入它的触发器范围后,如果玩家当前在自己巡逻区域内,则跟随追捕玩家。如果巡逻兵与玩家碰撞,则双方都播放碰撞后动画,游戏结束。

巡逻兵预制体

为巡逻兵添加两个Collider,一个是Capsule Collider,添加在预制体父节点上,用于检测巡逻兵与玩家的碰撞。另一个是Box Collider,添加在预制体的子节点上,用于检测玩家进入巡逻兵巡逻的范围

 

预制体:

 

 

 

巡逻兵创建:保存一些巡逻兵的基本数据

1
2
3
4
5
6
7
8
public class PatrolData : MonoBehaviour
{
    public int sign;                      //标志巡逻兵在哪一块区域
    public bool follow_player = false;    //是否跟随玩家
    public int wall_sign = -1;            //当前玩家所在区域标志
    public GameObject player;             //玩家游戏对象
    public Vector3 start_position;        //当前巡逻兵初始位置    
}

 

PropFactory

道具工厂,创建了9个巡逻兵,因为巡逻兵的位置有规律所以用循环就可以赋值初始位置,还设置了每个巡逻兵所在区域的标志。当游戏结束时候,需要工厂将巡逻兵的动画设置为初始状态。部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public List<GameObject> GetPatrols()
{
    int[] pos_x = { -6, 4, 13 };
    int[] pos_z = { -4, 6, -13 };
    int index = 0;
    //生成不同的巡逻兵初始位置
    for(int i=0;i < 3;i++)
    {
        for(int j=0;j < 3;j++)
        {
            vec[index] = new Vector3(pos_x[i], 0, pos_z[j]);
            index++;
        }
    }
    for(int i=0; i < 9; i++)
    {
        patrol = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol"));
        patrol.transform.position = vec[i];
        patrol.GetComponent<PatrolData>().sign = i + 1;
        patrol.GetComponent<PatrolData>().start_position = vec[i];
        used.Add(patrol);
    }  
    return used;
}
public void StopPatrol()
{
    //切换所有巡逻兵的动画
    for (int i = 0; i < used.Count; i++)
    {
        used[i].gameObject.GetComponent<Animator>().SetBool("run", false);
    }
}

  

巡逻兵巡逻与追捕

GoPatrolAction

巡逻兵巡逻的动作,根据四个方向来选择要去到的目的地,当当前位置与目的地相差0.9f的时候,换一个方向继续巡逻

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class GoPatrolAction : SSAction
{
    private enum Dirction { EAST, NORTH, WEST, SOUTH };
    private float pos_x, pos_z;                 //移动前的初始x和z方向坐标
    private float move_length;                  //移动的长度
    private float move_speed = 1.2f;            //移动速度
    private bool move_sign = true;              //是否到达目的地
    private Dirction dirction = Dirction.EAST;  //移动的方向
    private PatrolData data;                    //巡逻兵的数据
 
 
    private GoPatrolAction() { }
    public static GoPatrolAction GetSSAction(Vector3 location)
    {
        GoPatrolAction action = CreateInstance<GoPatrolAction>();
        action.pos_x = location.x;
        action.pos_z = location.z;
        //设定移动矩形的边长
        action.move_length = Random.Range(4, 7);
        return action;
    }
    public override void Update()
    {
        //防止碰撞发生后的旋转
        if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)
        {
            transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
        }           
        if (transform.position.y != 0)
        {
            transform.position = new Vector3(transform.position.x, 0, transform.position.z);
        }
        //巡逻兵移动
        Gopatrol();
        //如果巡逻兵需要跟随玩家并且玩家就在侦察兵所在的区域,侦查动作结束
        if (data.follow_player && data.wall_sign == data.sign)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this,0,this.gameobject);
        }
    }
    public override void Start()
    {
        this.gameobject.GetComponent<Animator>().SetBool("run", true);
        data  = this.gameobject.GetComponent<PatrolData>();
    }
 
    void Gopatrol()
    {
        if (move_sign)
        {
            //不需要转向则设定一个目的地,按照矩形移动
            switch (dirction)
            {
                case Dirction.EAST:
                    pos_x -= move_length;
                    break;
                case Dirction.NORTH:
                    pos_z += move_length;
                    break;
                case Dirction.WEST:
                    pos_x += move_length;
                    break;
                case Dirction.SOUTH:
                    pos_z -= move_length;
                    break;
            }
            move_sign = false;
        }
        this.transform.LookAt(new Vector3(pos_x, 0, pos_z));
        float distance = Vector3.Distance(transform.position, new Vector3(pos_x, 0, pos_z));
        //当前位置与目的地距离浮点数的比较
        if (distance > 0.9)
        {
            transform.position = Vector3.MoveTowards(this.transform.position, new Vector3(pos_x, 0, pos_z), move_speed * Time.deltaTime);
        }
        else
        {
            dirction = dirction + 1;
            if(dirction > Dirction.SOUTH)
            {
                dirction = Dirction.EAST;
            }
            move_sign = true;
        }
    }
}

  

PatrolFollowAction

巡逻兵朝着玩家的位置移动,移动结束的条件是玩家离开了巡逻兵触发器的范围或是玩家已经不在该区域内了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class PatrolFollowAction : SSAction
{
    private float speed = 2f;            //跟随玩家的速度
    private GameObject player;           //玩家
    private PatrolData data;             //侦查兵数据
 
    private PatrolFollowAction() { }
    public static PatrolFollowAction GetSSAction(GameObject player)
    {
        PatrolFollowAction action = CreateInstance<PatrolFollowAction>();
        action.player = player;
        return action;
    }
 
    public override void Update()
    {
        if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)
        {
            transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
        }
        if (transform.position.y != 0)
        {
            transform.position = new Vector3(transform.position.x, 0, transform.position.z);
        }
 
        Follow();
        //如果侦察兵没有跟随对象,或者需要跟随的玩家不在侦查兵的区域内
        if (!data.follow_player || data.wall_sign != data.sign)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this,1,this.gameobject);
        }
    }
    public override void Start()
    {
        data = this.gameobject.GetComponent<PatrolData>();
    }
    void Follow()
    {
        transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime);
        this.transform.LookAt(player.transform.position);
    }
}

  

SSActionManager

从上面可以看到,巡逻动作结束条件是需要追捕玩家,所以调用了回调函数,用回调函数来进行追捕动作。而当玩家离开追捕范围后,需要重新巡逻,也需要调用回调函数,从初始的位置和方向继续巡逻。除此之外,SSActionManager还实现了游戏结束后,摧毁所有动作,巡逻兵不再移动。部分代码如下public void SSActionEvent(SSAction source, int intParam = 0, GameObject objectParam = null)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<em id="__mceDel">{
    if(intParam == 0)
    {
        //侦查兵跟随玩家
        PatrolFollowAction follow = PatrolFollowAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().player);
        this.RunAction(objectParam, follow, this);
    }
    else
    {
        //侦察兵按照初始位置开始继续巡逻
        GoPatrolAction move = GoPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().start_position);
        this.RunAction(objectParam, move, this);
        //玩家逃脱
        Singleton<GameEventManager>.Instance.PlayerEscape();
    }
}
public void DestroyAll()
{
    foreach (KeyValuePair<int, SSAction> kv in actions)
    {
        SSAction ac = kv.Value;
        ac.destroy = true;
    }
}<br><br>
</em>

  

PatrolActionManager

初始的时候场景控制器调用PatrolActionManager中的方法,让巡逻兵开始巡逻,当游戏结束的时候,调用方法让巡逻兵停止巡逻

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PatrolActionManager : SSActionManager
{
    private GoPatrolAction go_patrol;                            //巡逻兵巡逻
 
    public void GoPatrol(GameObject patrol)
    {
        go_patrol = GoPatrolAction.GetSSAction(patrol.transform.position);
        this.RunAction(patrol, go_patrol, this);
    }
    //停止所有动作
    public void DestroyAllAction()
    {
        DestroyAll();
    }
}

  

玩家部分

该部分实现了玩家上下移动和左右旋转,并播放对应的动画以及相机的跟随。(相机跟随与上一个游戏代码一样)

UserGUI

在UserGUI得到用户的输入,然后调用场景控制器移动玩家的函数。部分代码如下

1
2
3
4
5
6
7
8
void Update()
{
    //获取方向键的偏移量
    float translationX = Input.GetAxis("Horizontal");
    float translationZ = Input.GetAxis("Vertical");
    //移动玩家
    action.MovePlayer(translationX, translationZ);
}

 

 

FirstSceneController

获得偏移量进行上下的移动,左右的旋转,播放对应的动画。部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//玩家移动
public void MovePlayer(float translationX, float translationZ)
{
    if(!game_over)
    {
        if (translationX != 0 || translationZ != 0)
        {
            player.GetComponent<Animator>().SetBool("run", true);
        }
        else
        {
            player.GetComponent<Animator>().SetBool("run", false);
        }
        //移动和旋转
        player.transform.Translate(0, 0, translationZ * player_speed * Time.deltaTime);
        player.transform.Rotate(0, translationX * rotate_speed * Time.deltaTime, 0);
        //防止碰撞带来的移动
        if (player.transform.localEulerAngles.x != 0 || player.transform.localEulerAngles.z != 0)
        {
            player.transform.localEulerAngles = new Vector3(0, player.transform.localEulerAngles.y, 0);
        }
        if (player.transform.position.y != 0)
        {
            player.transform.position = new Vector3(player.transform.position.x, 0, player.transform.position.z);
        }    
    }
}

区域部分

游戏场景中有9个格子,每个格子为一个区域,每个区域设置了一个Box Collider用于检测玩家是否进入该区域,(防止玩家在另一个区域但是进入了其他区域的巡逻兵的触发器时,巡逻兵隔着墙去追捕玩家的情况)

 

AreaCollide

每个区域有自己的标识(将脚本挂载在每个区域上进行设置),当玩家进入该区域的时候,会设置场景控制器的区域标识为自己的标识,这样其他的巡逻兵就知道玩家现在在哪个区域了

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AreaCollide : MonoBehaviour
{
    public int sign = 0;
    FirstSceneController sceneController;
    private void Start()
    {
        sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
    }
    void OnTriggerEnter(Collider collider)
    {
        //标记玩家进入自己的区域
        if (collider.gameObject.tag == "Player")
        {
            sceneController.wall_sign = sign;
        }
    }
}

  

订阅与发布模式部分

该部分的数值变化是通过订阅与发布模式实现的,FirstSceneController是模式中的订阅者。

发布事件类

GameEventManager

定义一个专门发布事件的类,订阅者可以订阅该类的事件,当其他类发生改变的时候,会使用GameEventManager的方法发布消息,触发相应事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class GameEventManager : MonoBehaviour
{
    //分数变化
    public delegate void ScoreEvent();
    public static event ScoreEvent ScoreChange;
    //游戏结束变化
    public delegate void GameoverEvent();
    public static event GameoverEvent GameoverChange;
    //水晶数量变化
    public delegate void CrystalEvent();
    public static event CrystalEvent CrystalChange;
 
    //玩家逃脱
    public void PlayerEscape()
    {
        if (ScoreChange != null)
        {
            ScoreChange();
        }
    }
    //玩家被捕
    public void PlayerGameover()
    {
        if (GameoverChange != null)
        {
            GameoverChange();
        }
    }
    //减少水晶数量
    public void ReduceCrystalNum()
    {
        if (CrystalChange != null)
        {
            CrystalChange();
        }
    }
}

  

订阅者

FirstSceneController

场景控制器作为订阅者,订阅了GameEventManager中的事件,只要相应事件发生,就会导致场景控制器调用注册的方法,部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void OnEnable()
{
    //注册事件
    GameEventManager.ScoreChange += AddScore;
    GameEventManager.GameoverChange += Gameover;
    GameEventManager.CrystalChange += ReduceCrystalNumber;
}
void OnDisable()
{
    //取消注册事件
    GameEventManager.ScoreChange -= AddScore;
    GameEventManager.GameoverChange -= Gameover;
    GameEventManager.CrystalChange -= ReduceCrystalNumber;
}
void ReduceCrystalNumber()
{
    recorder.ReduceCrystal();
}
void AddScore()
{
    recorder.AddScore();
}
void Gameover()
{
    game_over = true;
    patrol_factory.StopPatrol();
    action_manager.DestroyAllAction();
}

  

水晶触碰

CrystalCollide

当玩家触碰到水晶的时候,水晶消失,水晶数量减少

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CrystalCollide : MonoBehaviour
{
 
    void OnTriggerEnter(Collider collider)
    {
        if (collider.gameObject.tag == "Player" && this.gameObject.activeSelf)
        {
            this.gameObject.SetActive(false);
            //减少水晶数量,发布消息
            Singleton<GameEventManager>.Instance.ReduceCrystalNum();
        }
    }
}

  

玩家摆脱巡逻兵

SSActionManager

当玩家逃离巡逻兵触发器的范围的时候(此时巡逻兵的跟随玩家的动作会调用回调函数),分数会增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void SSActionEvent(SSAction source, int intParam = 0, GameObject objectParam = null)
{
    if(intParam == 0)
    {
        //侦查兵跟随玩家
        PatrolFollowAction follow = PatrolFollowAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().player);
        this.RunAction(objectParam, follow, this);
    }
    else
    {
        //侦察兵按照初始位置开始继续巡逻
        GoPatrolAction move = GoPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().start_position);
        this.RunAction(objectParam, move, this);
        //玩家逃脱,发布消息
        Singleton<GameEventManager>.Instance.PlayerEscape();
    }
}

  

玩家和巡逻兵碰撞

PlayerCollide

巡逻兵身上的碰撞器触碰到玩家的碰撞器将会使游戏结束,并且各自播放对应动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PlayerCollide : MonoBehaviour
{
 
    void OnCollisionEnter(Collision other)
    {
        //当玩家与巡逻兵相撞
        if (other.gameObject.tag == "Player")
        {
            other.gameObject.GetComponent<Animator>().SetTrigger("death");
            this.GetComponent<Animator>().SetTrigger("shoot");
            //游戏结束,发布消息
            Singleton<GameEventManager>.Instance.PlayerGameover();
        }
    }
}

  

上面都是参考师兄博客的,我自己从里面学习了架构

自己的改动,用鼠标控制方向,这样游戏就变得简单很多了!

 

 

具体代码请参考我的gitee,里面有Asset包

https://gitee.com/woodx9/unity-3D-homework/tree/master/AIPatrols

 

posted @   woodx  阅读(444)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
点击右上角即可分享
微信分享提示