模型与动画——智能巡逻兵

模型与动画——智能巡逻兵

一、提交要求:

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

二、具体玩法:

  • 玩家控制骷髅战士躲避巡逻兵,收集地图上的水晶
  • 方向键控制移动,长按移动可触发走和跑的转换
  • k键发动技能,技能发动2s内玩家无敌,并会杀死触碰到的巡逻兵

三、订阅发布模式MVC框架:

在这里插入图片描述

四、程序实现:

由于本次作业的还是沿用原有架构,并在此基础上加入了发布、订阅者的内容,所以重复部分就不再叙述

4.1 玩家部分实现:

玩家具有方向键移动功能,以及走和跑的动作转换和一个攻击技能

4.1.0 玩家预制和动画设置:

在这里插入图片描述

  • 首先加入我们的资源包,并加入刚体和碰撞组件
  • 为我们的玩家角色增加动画:
    • walk
    • run
    • skill
    • death
    • idle

整体状态机图:
在这里插入图片描述
参数设置:
在这里插入图片描述

  1. walk:
    可由idle状态转换,条件为run == true,我们设置转换的切换时间为很小,从而解决了动画转换的延迟问题
    在这里插入图片描述

  2. run:
    由walk转换而来,在玩家开始运动时,设置一个计时器,判断当持续移动超过3s后,触发状态转换,进入run状态,并提高移速;(因为要一直跑,所以设定一个trans指向run自己,从而实现动画的轮播)
    在这里插入图片描述

  3. skill:
    skill在任何状态下都可以触发,并且是瞬时发动,所以也要设置转换事件接近于0,并且由于任何状态都可以出发,所以要在其他所有状态都加入一个该状态的trans
    在这里插入图片描述
    这里由于我们的技能是瞬时发动,但是有持续事件,然后回到idle状态,所以我们在代码中设定一个计时器,2s后将对应参数设置回到默认,所以这里再播放完动画后,有0.几秒的定格事件,(其实将技能时间改短一点可以解决,但是游戏难度有点大,不放技能很难通关),因此有了延时触发返回idle的代码设置后。我们就将回到idle的转换效果也设置成瞬时的即可
    在这里插入图片描述

  4. death:
    当碰触巡逻兵时死亡,播放死亡动画:
    在这里插入图片描述

4.1.1 Interface:

interface中定义了玩家的所有操作:

public interface IUserAction                          
{
    //移动玩家
    void MovePlayer(float translationX, float translationZ);
    //得到分数
    int GetScore();
    //得到水晶数量
    int GetCrystalNumber();
    //得到游戏结束标志
    bool GetGameover();
    //重新开始
    void Restart();
    //释放技能
    void skill(bool sk);
}
4.1.2 UserGUI

获取用户的输入,之后调用函数来进行玩家的移动和技能释放等操作,同时进行界面的管理,显示字幕等信息

    void Update()
    {
        //获取方向键的偏移量
        float translationX = Input.GetAxis("Horizontal");
        float translationZ = Input.GetAxis("Vertical");
        sk = Input.GetKeyDown(KeyCode.K);
        //判断技能的释放
        action.skill(sk);
        //移动玩家
        action.MovePlayer(translationX, translationZ);
    }

    private void OnGUI()
    {
        GUI.Label(new Rect(10, 5, 200, 50), "分数:", text_style);
        GUI.Label(new Rect(55, 5, 200, 50), action.GetScore().ToString(), score_style);
        GUI.Label(new Rect(Screen.width - 170, 5, 50, 50), "剩余水晶数:", text_style);
        GUI.Label(new Rect(Screen.width - 80, 5, 50, 50), action.GetCrystalNumber().ToString(), score_style);
        if(action.GetGameover() && action.GetCrystalNumber() != 0)
        {
            GUI.Label(new Rect(Screen.width / 2 - 50, 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.GetCrystalNumber() == 0)
        {
            GUI.Label(new Rect(Screen.width / 2 - 50, 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;
            }
        }
        if(show_time > 0)
        {
            GUI.Label(new Rect(Screen.width / 2-80 ,10, 100, 100), "按WSAD或方向键移动,k键发动技能", text_style);
            GUI.Label(new Rect(Screen.width / 2 - 87, 30, 100, 100), "成功躲避巡逻兵追捕加1分", text_style);
            GUI.Label(new Rect(Screen.width / 2 - 90, 50, 100, 100), "采集完所有的水晶即可获胜", text_style);
        }
    }
4.1.3 FirstSceneController

根据userGUI中的传入参数来进行具体的操作,决定玩家人物的移动和技能释放等行为

    public void MovePlayer(float translationX, float translationZ)
    {
        if(!game_over)
        {
            if (translationX != 0 || translationZ != 0)
            {
                player.GetComponent<Animator>().SetBool("run", true);
                run_time = player.GetComponent<Animator>().GetFloat("start_run");
                run_time += Time.deltaTime;
                if(run_time > 3)
                {
                    player_speed = 7;
                    player.GetComponent<Animator>().SetFloat("speed", player_speed);
                }
                player.GetComponent<Animator>().SetFloat("start_run",run_time);
            }
            else
            {
                player.GetComponent<Animator>().SetBool("run", false);
                player.GetComponent<Animator>().SetFloat("start_run", 0);
                    player.GetComponent<Animator>().SetFloat("speed", 0);
                    player_speed = 5;
            }
            //移动和旋转
            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 void skill(bool sk)
    {
        if (sk)
        {
            Debug.Log("sk is :" + sk);
            if (!skill_bool)
            {
                skill_bool = true;
                skill_time = 0;
                player.GetComponent<Animator>().SetBool("skill", true);
            }
            else
            {
                skill_time += Time.deltaTime;
            }
        }
        else
        {
            if (skill_bool)
            {
                skill_time += Time.deltaTime;
            }
        }
        if(skill_time > 2)
        {
            skill_bool = false;
            player.GetComponent<Animator>().SetBool("skill", false);
        }
        
    }

4.2 巡逻兵部分

实现巡逻兵进行矩形路线的巡逻,和玩家进入范围内后的追踪功能,以及碰撞和边界的设定

4.2.0 动画设置:
  • 巡逻兵模型:小圈为碰撞体积,判断与玩家和墙壁碰撞,大圈为感性碰撞体,当玩家进入后,开始追踪
    在这里插入图片描述

  • 动作设置:

在这里插入图片描述

  • run:正常状态下一直为run因为设定和idle为瞬时切换,所以一直现实的为run动作
    在这里插入图片描述

  • shoot:当碰触到玩家时,玩家死亡,巡逻兵播放射击动画
    在这里插入图片描述

  • death:当碰触到释放技能的玩家时,自身死亡
    在这里插入图片描述

4.2.1 Patrol Data

巡逻兵的属性存储

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

巡逻兵工厂类,用于生产和回收巡逻兵

    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 List<GameObject> GetCrystal()
    {
        for(int i=0;i<12;i++)
        {
            crystal = Instantiate(Resources.Load<GameObject>("Prefabs/Crystal"));
            float ranx = Random.Range(-range, range);
            float ranz = Random.Range(-range, range);
            crystal.transform.position = new Vector3(ranx, 0, ranz);
            usedcrystal.Add(crystal);
        }

        return usedcrystal;
    }
    public void StopPatrol()
    {
        //切换所有侦查兵的动画
        for (int i = 0; i < used.Count; i++)
        {
            used[i].gameObject.GetComponent<Animator>().SetBool("run", false);
        }
    }
4.2.3 GoPatrolAction

巡逻兵的移动动作,按照举行运动,同时添加死亡后的停止判断

void Gopatrol()
    {
        if (!this.gameobject.GetComponent<Animator>().GetBool("death"))
        {
            if (this.gameobject.GetComponent<Animator>().GetBool("death"))
            {
                return;
            }
            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;
            }
        }

    }
4.2.4 PatrolFollowAction

巡逻兵发现玩家后跟随玩家运动动作

    void Follow()
    {
        transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime);
        this.transform.LookAt(player.transform.position);
    }

    public override void Update()
    {
        if (!this.gameobject.GetComponent<Animator>().GetBool("death"))
        {
            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);
            }
        }

    }
4.2.5 SSActionManager

实现玩家逃离后的回调函数实现,以及游戏结束后动作的停止

    public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)
    {
        action.gameobject = gameobject;
        action.transform = gameobject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }

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


    public void DestroyAll()
    {
        foreach (KeyValuePair<int, SSAction> kv in actions)
        {
            SSAction ac = kv.Value;
            ac.destroy = true;
        }
    }
4.2.6 PlayerCollide

玩家碰撞类,挂载在巡逻兵身上,与玩家碰撞时触发,根据玩家的状态来判断玩家死亡还是巡逻兵死亡

public class PlayerCollide : MonoBehaviour
{

    void OnCollisionEnter(Collision other)
    {
        //当玩家与侦察兵相撞
        if (other.gameObject.tag == "Player")
        {
            Debug.Log("adfasdf");
            if (other.gameObject.GetComponent<Animator>().GetBool("skill"))
            {
                this.GetComponent<Animator>().SetBool("death",true);
                //Destroy(this.GetComponent<PatrolData>());
            }
            else if(this.GetComponent<Animator>().GetBool("death"))
            {

            }
            else
            {
                other.gameObject.GetComponent<Animator>().SetTrigger("death");
                this.GetComponent<Animator>().SetTrigger("shoot");
                Singleton<GameEventManager>.Instance.PlayerGameover();
            }

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

4.3 订阅发布模式

4.3.1 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 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();
        }
    }
}
4.3.2 订阅事件FirstSceneController

场景控制器订阅相应的事件,当触发时,调整对应的参数和调用相关函数

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

五、游戏效果:

  • walk:
    在这里插入图片描述

  • run:

在这里插入图片描述

  • skill:

在这里插入图片描述

  • death:

在这里插入图片描述

六、gitee地址:

gitee

七、个人总结:

本次作业主要了解了动作状态机的使用,尝试进行了动作的切换等管理,掌握了基础运动,遇到的小困难是一开始,设置跳转的优先级,重复播放的设置和切换的瞬时切换和切换延迟的设定

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值