[Unity-3D] 小女孩与巡逻僵尸

小女孩与巡逻僵尸


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

规则介绍
玩家使用ws或者↑↓控制角色前进/后退,使用ad或者←→控制角色向左向右转。控制角色避开僵尸的追击,每逃避一次就加一分,最高记录将一直显示。
视频演示
[Unity-3d]小女孩与鬼畜巡逻僵尸


订阅发布模式

本次作业要求使用订阅发布模式实现。简单来说,订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。该模式中发布者和订阅者没有直接的耦合,是实现模型与视图分离的重要手段。具体到这次的作业,将涉及到逃离僵尸追踪和被僵尸追上等事件需要监听,定义一个发布事件类,以及在场记类中实现订阅者,代码如下:

发布者

// 发布事件类 EventManager.cs 中的发布者

    // 时间响应:分数变化
    public delegate void scoreEvent();
    public static event scoreEvent changeScore;
    // 事件响应:游戏结束
    public delegate void gameOverEvent();
    public static event gameOverEvent gameOver;

    // 事件1:逃离僵尸
    public void PlayerEscape()
    {
        if (changeScore)
            changeScore();
    }
    // 事件2:被僵尸追上
    public void PlayerGameover()
    {
        if (gameOver)
            gameOver();
    }

订阅者

// 场记类 SceneController.cs 中的订阅者
    public PatrolFactory patrolFactory;                   // 僵尸工厂
    public ScoreRecorder recorder;                      // 记分牌
    public PatrolActionManager actionManager;           // 僵尸动作管理者
    private bool isOver = false;                        // 游戏结束标识

    void OnEnable()                                     // 注册订阅
    {
        EventManager.changeScore += AddScore;       
        EventManager.gameOver += GameOver;    
    }
    void OnDisable()                                    //取消订阅
    {
        EventManager.changeScore -= AddScore;       
        EventManager.gameOver -= GameOver;    
    }
    void AddScore()                                     // 记分牌变化
    {
        recorder.addScore();                                // 加分 
    }
    void GameOver()                                     // 游戏结束变化
    {
        isOver = true;                                      // 游戏结束
        patrolFactory.reset();                              // 重置巡逻僵尸位置
        actionManager.freezeAllAction();                    // 停止所有僵尸动作
    }

游戏具体部分实现

其实要实现的部分还算是比较多的,主要有巡逻僵尸部分、玩家角色部分、UI、记分牌等等,接下来将说一下核心的代码部分。

地图

首先当然是搭建地图啦,大家可以在AssetsStore上找到很多很好看的又免费的资源,但是为了节约时间,这一次我就弄了一个比较简单的九宫地图。
地图
一个值得注意的问题就是如何防止僵尸们越界追人,于是我给每一个房间设了一个BoxCollider(如上图所示),这样当小女孩跟某一个BoxCollider有接触时,我们就可以知道她在哪个房间,就可以只允许对应房间僵尸运动了。

// ColliderBlock.cs
    public int block = 0;
    void OnTriggerEnter(Collider collider)
    {
        // 若小女孩进入该房间,更新p_block
        if (collider.gameObject.tag == "Player")
        {
            sceneController.p_block = sign;
        }
    }

巡逻僵尸

首先,是把最基础的预制设置好,进入AssetsStore,本来是要搜索soilders的,结果在搜索结果里面发现了这么一套可爱的小人,还是免费的,果断下载。
预制小人
OK,然后我们要做的是给巡逻僵尸加上Animator、CapsuleCollider、Rigidbody组件,分别是为了实现动画效果、添加碰撞体、添加刚体。除此之外,我们还要在他的子结点添加一个BoxCollider,并调整它的大小,这个碰撞体其实是实现一个探测器的效果,当小女孩与其碰撞时,就相当于小女孩进入僵尸的探测范围啦。
接下来就是巡逻僵尸的相关类实现啦。

巡逻僵尸基本属性类

// PatrolData.cs
    public int block;                   // 巡逻兵当前区域
    public int p_block = -1;            // 小女孩当前区域
    public bool isChasing = false;      // 是否追踪小女孩

    public GameObject player;           // 玩家游戏对象
    public Vector3 start_position;      // 当前巡逻兵初始位置     

巡逻僵尸工厂类

// PatrolFactory.cs
    private GameObject temp = null;                              // 临时对象
    private List<GameObject> patrols = new List<GameObject>();   // 巡逻僵尸队列
    private Vector3[] pos = new Vector3[9];                      // 巡逻僵尸们的初始位置

    public List<GameObject> GetPatrols()
    {
        int[] pos_x = { -5, 5, 15 };
        int[] pos_z = { -5, 5, 15 };
        int index = 0;

        // 设置初始位置
        for(int i = 0; i < 3; i++)
            for(int j = 0; j < 3; j++)
                pos[index++] = new Vector3(pos_x[i], 0, pos_z[j]);

        // 放置僵尸    
        for(int i = 0; i < 9; i++)
        {
            temp = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol2"));
            temp.transform.position = pos[i];
            temp.GetComponent<PatrolData>().sign = i + 1;
            temp.GetComponent<PatrolData>().start_position = pos[i];
            patrols.Add(temp);
        }   
        return patrols;
    }


    public void reset() // 重置巡逻僵尸
    {
        for (int i = 0; i < used.Count; i++)
            used[i].gameObject.GetComponent<Animator>().SetBool("run", false);
    }
}

巡逻僵尸动作类
僵尸巡逻

// PatrolGo.cs 僵尸巡逻
    private enum Direction { EAST, NORTH, WEST, SOUTH };
    private float pos_x, pos_z;                     //移动前的初始x和z方向坐标
    private float length;                           //移动的长度
    private float p_speed = 1.2f;                   //巡逻移动速度
    private bool isEnd = true;                      //是否到达路径终点
    private Direction dir = Direction.EAST;         //移动的方向
    private PatrolData data;                        //巡逻僵尸的属性

    public static PatrolGon getInstance(Vector3 loc, GameObject p)
    {
        PatrolGo instance = CreateInstance<PatrolGo>();
        instance.pos_x = loc.x;
        instance.pos_z = loc.z;
        instance.length = Random.Range(4, 7);
        return instance;
    }

    public override void Start()
    {
        this.gameobject.GetComponent<Animator>().SetBool("run", true);
        data = this.gameobject.GetComponent<PatrolData>();
    }

    public override void Update()
    {
        if (isEnd)
        {
            switch (dir)
            {
                case Direction.EAST:
                    pos_x += length;
                    break;
                case Direction.WEST:
                    pos_x -= length;
                    break;
                case Direction.NORTH:
                    pos_z += length;
                    break;
                case Direction.SOUTH:
                    pos_z -= length;
                    break;
            }
            isEnd = false;
        }
        Vector3 des = new Vector3(pos_x, 0, pos_z);
        this.transform.LookAt(des);
        if (Vector3.Distance(this.transform.position, des) > 0)
            this.transform.position = 
                Vector3.MoveTowards(this.transform.position, des, p_speed * Time.deltaTime);
        else {
            dir++;
            if (dir > Direction.SOUTH)
                dir = Direction.EAST;
            isEnd = true;
        }


        //若僵尸探测到玩家,巡逻动作结束,开始追踪
        if (data.follow_player && data.wall_sign == data.sign)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this,0,this.gameobject);
        }
    }

僵尸追踪

// PatrolChase.cs 僵尸追踪
    private float speed = 2f;            // 追踪玩家的速度
    private GameObject player;           // 玩家
    private PatrolData data;             // 巡逻僵尸属性

    public static PatrolFollowAction GetSSAction(GameObject player)
    {
        PatrolFollowAction action = CreateInstance<PatrolFollowAction>();
        action.player = player;
        return action;
    }

    public override void Start()
    {
        data = this.gameobject.GetComponent<PatrolData>();
    }

    public override void Update()
    {
        this.transform.LookAt(player.transform.position);
        this.transform.position = 
            Vector3.MoveTowards(this.transform.position, player.transform.position, c_speed * Time.deltaTime);

        //若僵尸探测不到玩家,追踪动作结束,开始巡逻
        if (!data.follow_player || data.wall_sign != data.sign)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this,1,this.gameobject);
        }
    }

以上动作类实现完之后,即可在动作管理类中调用。如在结束巡逻并使用回调函数后追踪玩家,又或者是丢失玩家并使用回调函数重新巡逻。

小女孩

相对僵尸巡逻和追踪两种行动模式来说,小女孩的操作就简单多了,只需要通过GUI让玩家控制小女孩的行动就好。

UI类

// UserInterface.cs 
    private IUserAction action;

    void Update()
    {
        // 键入方向键,移动小女孩
        float change_x = Input.GetAxis("Horizontal");
        float change_z = Input.GetAxis("Vertical");
        action.MovePlayer(change_x, change_z);
    }

场景控制类

// SceneController.cs
    //玩家移动
    public void MovePlayer(float change_x, float change_z)
    {
        if(!game_over)
        {
            // 若在走动则设置run动画,否则静止
            if (change_x != 0 || change_z != 0)
                player.GetComponent<Animator>().SetBool("run", true);
            else
                player.GetComponent<Animator>().SetBool("run", false);

            // 移动小女孩
            player.transform.Translate(0, 0, change_z * player_speed * Time.deltaTime);
            player.transform.Rotate(0, change_x * 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);

        }
    }

各种交互事件

僵尸的探测器 与 小女孩 交互

// ColliderDetector.cs
    void ChasingFlag(Collider girl)
    {
        if (girl.gameObject.tag == "Player")
        {
            // 小女孩被探测到
            this.gameObject.transform.parent.GetComponent<PatrolData>().isChasing = true;
            this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject;
        }
    }

    void EscapeFlag(Collider collider)
    {
        if (collider.gameObject.tag == "Player")
        {
            // 小女孩脱离探测范围
            this.gameObject.transform.parent.GetComponent<PatrolData>().isChasing = false;
            this.gameObject.transform.parent.GetComponent<PatrolData>().player = null;
        }
    }

僵尸 与 小女孩 交互

// ColliderPatrol.cs
    void collide(Collision collider)
    {
        if (collider.gameObject.tag == "Player")
        {
            // 小女孩与僵尸相碰撞
            colliderr.gameObject.GetComponent<Animator>().SetTrigger("death");
            Singleton<GameEventManager>.Instance.PlayerGameover();
        }
    }

小结

以上便是基本的核心代码了,还有一些记分牌、摄像头跟踪什么的,这里不做赘述,都是一些可以随意发挥的东西。本次作业就写到这里了,特别鸣谢C486C大神的博客,在下实在是佩服的五体投地hhh。

猜你喜欢

转载自blog.csdn.net/eddie_peng/article/details/80287220