Unity3d模型与动画 智能巡逻兵游戏


游戏设计:
• 创建一个地图和若干巡逻兵(使用动画);
• 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
• 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
• 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
• 失去玩家目标后,继续巡逻;
• 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
程序设计:
• 使用订阅与发布模式传消息
• 工厂模式生产巡逻兵

游戏设计

基于上面的几点,我们设计这样一个游戏场景:
1.我们的地图有3×3共计9块区域,不同区域间有部分连通,每个区域有一个巡逻兵。
2.地图上会随机生成一些任务点,作为玩家需要到达的目标位置,这些任务点需要玩家全部到达且未被巡逻兵碰撞到方为获胜。
3.每到达一个任务点时为甩掉巡逻兵一次,计一分。
4.每次游戏任务点位置随机分布在地图上,但总的任务点数量一定。
5.玩家初始位置为地图区域的中心位置。

游戏展示

游戏操作演示1-游戏各项操作(11.7M)
游戏操作演示2-失败(1.17M)
游戏操作演示3-完成(0.9M)

游戏预制制作

基于上面的要求与设计,可以先为我们的游戏场景创建4个预制:
地图、玩家、“任务点”、巡逻兵。
地图我使用了Low Poly v2资源包进行预制制作。

玩家和巡逻兵我基于SoldierFree资源包中的预制制作,我们为两个预制添加Rigidbody和Capsule Collider来实现玩家与巡逻兵的碰撞。
对于巡逻兵的预制,我们添加了两个脚本,一个脚本用于记录巡逻兵对象的一些信息,另一个脚本用于处理玩家与巡逻兵相撞后的情况。另外我们添加了一块方形的碰撞器(设置为触发器)来探测玩家,并添加了相应的脚本来使得有玩家进入/离开该方形区域时,巡逻兵开始/停止追捕玩家。
我们使用了Animator组件,通过控制器来使得对应的对象做出相应的动画行为。它是使用状态机来管理运动的,我们在脚本中可以设置状态的改变,从而对应有对象(玩家或巡逻兵)的动画表现。然后我们将玩家对象和巡逻兵对象对应的控制器加到预制上。

“任务点”目前是简单的用红色圆球来代替(其碰撞体积也为圆球)。

这样我们就拥有了一些游戏的基本元素。

脚本编写

我们的实现要求是:订阅与发布模式传消息和工厂模式生产巡逻兵。

我们先给出主摄像机上附加脚本CameraMove,从而我们可以在移动玩家游戏对象时伴随镜头的移动。

public class CameraMove : MonoBehaviour
{
    public GameObject follow;  // 跟随的对象
    public float speed = 5f;  // 相机跟随的速度
    Vector3 offset;  // 相机与物体相对偏移位置

    void Start()
    {
        offset = transform.position - follow.transform.position;
    }

    void FixedUpdate()
    {
        Vector3 target = follow.transform.position + offset;  // 计算目标位置
        transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime);  // 摄像机自身位置到目标位置平滑过渡
    }
}

对于我们的巡逻兵,我们有一个脚本PatrolData附加其上,用于保存它的一些信息

public class PatrolData : MonoBehaviour
{
    public int block;  // 巡逻兵所在区域编号
    public bool follow_player = false;  // 是否正在追逐玩家
    public int player_block = -1;  // 此时玩家所在区域编号
    public GameObject player;  // 玩家游戏对象
    public Vector3 start_position;  // 巡逻兵初始位置
}

接着我们给出几个预制中与碰撞相关的脚本(里面使用了控制器等部分后续给出)。
首先对于场景,我们对9块区域都附加上PlaneCollide脚本,以更新玩家所在的区域信息。

public class PlaneCollide : MonoBehaviour
{
    public int block = 0;
    FirstSceneController sceneController;
    private void Start()
    {
        sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
    }
    void OnTriggerEnter(Collider collider)
    {
        // 标记玩家进入区域
        if (collider.gameObject.tag == "Player")
        {
            sceneController.player_block = block;
        }
    }
}

然后对于巡逻兵,由于游戏失败的条件是由巡逻兵与玩家相撞,故我们附加脚本PatrolPlayerCollide处理这一情况:

public class PatrolPlayerCollide : 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();  // 游戏结束
        }
    }
}

巡逻兵还需要对其附近的区域进行探测,当玩家进入其探测区域,巡逻兵会对玩家进行追逐,故我们有脚本PatrolCollide

public class PatrolCollide : MonoBehaviour
{
    void OnTriggerEnter(Collider collider)
    {
        if (collider.gameObject.tag == "Player")  // 玩家进入巡逻兵探测范围
        {
            this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = true;
            this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject;
        }
    }
    void OnTriggerExit(Collider collider)
    {
        if (collider.gameObject.tag == "Player")  // 玩家离开巡逻兵探测范围
        {
            this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = false;
            this.gameObject.transform.parent.GetComponent<PatrolData>().player = null;
        }
    }
}

对于碰撞,我们还有一种情况需要处理——当玩家与我们的“任务点”碰撞,“任务点”消失,需要完成的任务数减少。我们有脚本TaskCollide

public class TaskCollide : MonoBehaviour
{
    void OnTriggerEnter(Collider collider)
    {
        if (collider.gameObject.tag == "Player" && this.gameObject.activeSelf)
        {
            this.gameObject.SetActive(false);
            Singleton<GameEventManager>.Instance.ReduceTaskNum();  // 减少任务数量
        }
    }
}

然后我们运用工厂模式生产巡逻兵(和“任务点”),我们有工厂类Factory。可以看到我们巡逻兵的出生点是固定的(玩家出生点也是固定的,避免二者开局就相遇),而任务点是在地图中随机刷新的。

public class Factory : MonoBehaviour
{
    private GameObject patrol = null;  // 巡逻兵
    private List<GameObject> used = new List<GameObject>(); // 正在被使用的巡逻兵
    private GameObject task = null;  // 任务点
    private List<GameObject> usedtask = new List<GameObject>();  // 正在被使用的任务点
    private float range = 12;  // 任务点生成的坐标范围
    private Vector3[] vec = new Vector3[9];  // 用于保存每个巡逻兵的初始位置
    public FirstSceneController sceneControler;  // 场景控制器

    public List<GameObject> GetPatrols()  // 生成巡逻兵
    {
        int[] pos_x = { -6, 4, 13 };
        int[] pos_z = { -4, 6, -13 };
        for(int i=0;i < 3;i++)  // 生成不同的巡逻兵初始位置
        {
            for(int j=0;j < 3;j++)
            {
                vec[i*3+j] = new Vector3(pos_x[i], 0, pos_z[j]);
            }
        }
        for(int i=0; i < 9; i++)  // 依次生成放置巡逻兵
        {
            patrol = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol"));
            patrol.transform.position = vec[i];
            patrol.GetComponent<PatrolData>().block = i + 1;
            patrol.GetComponent<PatrolData>().start_position = vec[i];
            used.Add(patrol);
        }   
        return used;
    }


    public List<GameObject> GetTask()  // 生成任务点
    {
        for(int i=0;i<12;i++)
        {
            task = Instantiate(Resources.Load<GameObject>("Prefabs/Task"));
            float ranx = Random.Range(-range, range);
            float ranz = Random.Range(-range, range);
            task.transform.position = new Vector3(ranx, 0, ranz);
            usedtask.Add(task);
        }

        return usedtask;
    }
    public void StopPatrol()  // 游戏结束,使所有巡逻兵停止运动
    {
        for (int i = 0; i < used.Count; i++)
        {
            used[i].gameObject.GetComponent<Animator>().SetBool("run", false);  // 切换状态
        }
    }
}

这样我们就定义了工厂类来生成巡逻兵,我们游戏最核心的控制部分为SceneController,这个控制器里我们就调用它来生成了巡逻兵。对于该控制器,我们应该让其最先运行,所以我们要在项目设置-脚本执行顺序中加入它,并把它放在较前的位置,如下图:

该控制器串联起了游戏的各个部分,其内容如下:

using UnityEngine.SceneManagement;

public class SceneController : MonoBehaviour, IUserAction, ISceneController
{
    public Factory patrol_factory;  // 巡逻兵工厂
    public ScoreRecorder recorder;  // 记录得分
    public PatrolActionManager action_manager;  // 运动管理器
    public int player_block = -1;  // 当前玩家所处格子编号
    public GameObject player;  // 玩家
    public Camera main_camera;  // 主相机
    public float player_speed = 5;  // 玩家移动速度
    public float rotate_speed = 135f;  // 玩家旋转速度
    private List<GameObject> patrols;  // 地图中巡逻兵列表
    private List<GameObject> tasks;  // 地图中任务列表
    private bool game_over = false;  // 游戏结束状态

    void Update()
    {
        for (int i = 0; i < patrols.Count; i++)  // 更新所有巡逻兵掌握的玩家区位信息
        {
            patrols[i].gameObject.GetComponent<PatrolData>().player_block = player_block;
        }
        if(recorder.GetTaskNumber() == 0)  // 任务全部完成
        {
            Gameover();
        }
    }
    void Start()
    {
        SSDirector director = SSDirector.GetInstance();
        director.CurrentScenceController = this;
        patrol_factory = Singleton<Factory>.Instance;
        action_manager = gameObject.AddComponent<PatrolActionManager>() as PatrolActionManager;
        LoadResources();
        main_camera.GetComponent<CameraMove>().follow = player;
        recorder = Singleton<ScoreRecorder>.Instance;
    }

    public void LoadResources()
    {
        Instantiate(Resources.Load<GameObject>("Prefabs/Plane"));
        player = Instantiate(Resources.Load("Prefabs/Player"), new Vector3(0, 9, 0), Quaternion.identity) as GameObject;
        tasks = patrol_factory.GetTask();  // 生成任务点
        patrols = patrol_factory.GetPatrols();  // 生成巡逻兵
        for (int i = 0; i < patrols.Count; i++)  // 使所有巡逻兵移动
        {
            action_manager.GoPatrol(patrols[i]);
        }
    }

    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);
            }     
        }
    }

    public int GetScore()
    {
        return recorder.GetScore();
    }

    public int GetTaskNumber()
    {
        return recorder.GetTaskNumber();
    }
    public bool GetGameover()
    {
        return game_over;
    }
    public void Restart()
    {
        SceneManager.LoadScene("Scenes/mySence");
    }

    void OnEnable()
    {
        GameEventManager.ScoreChange += AddScore;
        GameEventManager.GameoverChange += Gameover;
        GameEventManager.TaskChange += ReduceTaskNumber;
    }
    void OnDisable()
    {
        GameEventManager.ScoreChange -= AddScore;
        GameEventManager.GameoverChange -= Gameover;
        GameEventManager.TaskChange -= ReduceTaskNumber;
    }
    void ReduceTaskNumber()
    {
        recorder.ReduceTask();
    }
    void AddScore()
    {
        recorder.AddScore();
    }
    void Gameover()
    {
        game_over = true;
        patrol_factory.StopPatrol();
        action_manager.DestroyAllAction();
    }
}

在我们的场景控制器中,它作为订阅者,订阅了GameEventManager中的事件。有对应的事件发生,场景控制器就会调用相应的方法,GameEventManager部分如下:

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 TaskEvent();  // 剩余任务数量变化
    public static event TaskEvent TaskChange;

    public void PlayerEscape()  // 玩家逃脱追捕
    {
        if (ScoreChange != null)
        {
            ScoreChange();
        }
    }

    public void PlayerGameover()  // 玩家被追到
    {
        if (GameoverChange != null)
        {
            GameoverChange();
        }
    }

    public void ReduceTaskNum()  // 剩余任务数量减少
    {
        if (TaskChange != null)
        {
            TaskChange();
        }
    }
}

而像我们上面给出的TaskCollidePatrolPlayerCollide中,也有消息的发布,它们发布的消息都会被“订阅者”场景控制器接收到做相应的处理。
除此之外,我们还有在玩家逃脱巡逻兵追捕时,也会发布消息,使得场景控制器变更得分。这部分涉及到巡逻兵的移动动作——
巡逻兵主要在干两件事情:一是巡逻,这时玩家在其探测区域之外(或者并不在该巡逻兵所在的区块);二是追逐。
首先我们有SSAction类作为游戏的动作基类。相应的,我们有动作管理器SSActionManager。我们里面有向订阅者“通报消息”的部分,因此运动状态的变化会影响场景。我们还有游戏的总导演SSDirector
基类的代码我们不再给出,我们给出巡逻兵的运动的实现,这包括三部分:
1.巡逻部分的脚本PatrolGoAction:

public class PatrolGoAction : 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 PatrolGoAction() { }
    public static PatrolGoAction GetSSAction(Vector3 location)
    {
        PatrolGoAction action = CreateInstance<PatrolGoAction>();
        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.player_block == data.block)  // 如果巡逻兵需要跟随玩家,且玩家在巡逻兵所在区域,停止巡查开始追捕
        {
            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;
        }
    }
}

2.追逐部分的脚本PatrolFollowAction

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.player_block != data.block)  // 如果巡逻兵无需跟随玩家/玩家不再巡逻兵所在区域,停止追捕开始巡逻
        {
            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);
    }
}

3.巡逻兵的动作管理器PatrolActionManager

public class PatrolActionManager : SSActionManager
{
    private PatrolGoAction go_patrol; // 巡逻兵巡逻
    public void GoPatrol(GameObject patrol)
    {
        go_patrol = PatrolGoAction.GetSSAction(patrol.transform.position);
        this.RunAction(patrol, go_patrol, this);
    }
    public void DestroyAllAction()  // 停止所有动作
    {
        DestroyAll();
    }
}

最后是我们游戏的图形化界面UserGUI部分,该部分向玩家提供了大量的游戏信息:

public class UserGUI : MonoBehaviour {

    private IUserAction action;
    private GUIStyle score_style = new GUIStyle();
    private GUIStyle text_style = new GUIStyle();
    private GUIStyle over_style = new GUIStyle();
    public int game_time = 0;
    void Start ()
    {
        action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
        text_style.normal.textColor = new Color(0, 0, 0, 1);
        text_style.fontSize = 16;
        score_style.normal.textColor = new Color(1,0.92f,0.016f,1);
        score_style.fontSize = 16;
        over_style.fontSize = 25;
        StartCoroutine(GameTimer());  
    }

    void Update()
    {
        //获取方向键的偏移量
        float translationX = Input.GetAxis("Horizontal");
        float translationZ = Input.GetAxis("Vertical");
        //移动玩家
        action.MovePlayer(translationX, translationZ);
    }
    private void OnGUI()
    {
        GUI.Label(new Rect(Screen.width - 170, 10, 50, 50), "剩余任务数:", text_style);
        GUI.Label(new Rect(Screen.width - 80, 10, 50, 50), action.GetTaskNumber().ToString(), score_style);
        GUI.Label(new Rect(Screen.width - 170, 30, 50, 50), "当前得分:", text_style);
        GUI.Label(new Rect(Screen.width - 80, 30, 50, 50), action.GetScore().ToString(), score_style);
        GUI.Label(new Rect(Screen.width - 170, 50, 50, 50), "当前用时:", text_style);
        GUI.Label(new Rect(Screen.width - 80, 50, 50, 50), game_time.ToString(), score_style);
        GUI.Label(new Rect(20 ,10, 100, 100), "游戏提示", text_style);
        GUI.Label(new Rect(20, 30, 100, 100), "按WSAD或方向键移动", text_style);
        GUI.Label(new Rect(20, 50, 100, 100), "成功躲避巡逻兵追捕加1分到达一个任务点", text_style);
        GUI.Label(new Rect(20, 70, 100, 100), "你需要到达所有的任务点", text_style);
        if(action.GetGameover() && action.GetTaskNumber() != 0)
        {
            GUI.Label(new Rect(Screen.width / 2 - 100, Screen.width / 2 - 250, 100, 100), "遗憾,任务失败。", over_style);
            if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "再来一局"))
            {
                action.Restart();
                return;
            }
        }
        else if(action.GetTaskNumber() == 0)
        {
            GUI.Label(new Rect(Screen.width / 2 - 100, Screen.width / 2 - 250, 100, 100), "恭喜,完成任务!", over_style);
            if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "再来一局"))
            {
                action.Restart();
                return;
            }
        }
    }

    public IEnumerator GameTimer()
    {
        while (!action.GetGameover())
        {
            yield return new WaitForSeconds(1);
            game_time++;
        }
    }
}

总之,在上面的实现中,我们运用了发布与订阅模式(它可以定义了一种一对多的依赖关系,实现了让多个订阅者对象同时监听某一个主题对象。当监听到变化时,订阅者即可据此做出相应的变化。)和工厂模式(批量生成了巡逻兵)。

本人的“3D游戏编程与设计”系列合集,请访问:
https://www.yizuodi.cn/category/3DGame/

声明:一座堤的博客|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Unity3d模型与动画 智能巡逻兵游戏


为者常成 行者常至