模型与动画

模型与动画

这是3D游戏编程的第七次作业

说明文档

本次实验完成了所有基本要求,尽量将步骤展示出。
闪光点:
动画状态机制作细节、预制制作细节
详细类图以及代码注释

作业内容

智能巡逻兵

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

效果展示

声明:本次实验使用了优秀博客中的预制,偷了个懒,但是游戏的状态机动画及碰撞体设置等都由自己完成,并基于优秀博客给出了我认为比较合适的设计(原优秀博客的代码设计是比较乱的)

  • 巡逻兵只在自己的区域内巡逻,不会跨区域追踪
  • 脱离追踪时能够获得一分(动图中在最后脱离第一个巡逻兵后分数从3变成4)
  • 碰到巡逻兵后游戏结束,其他巡逻兵停止动画

设计与实现

状态机制作
  • 玩家状态机

    注意,在玩家动画状态机中,有必要将Idle到Run变化的转变中取消勾选has exit time,否则将会出现如果持续按前进,动画会一直停在Idle,这是因为:

    hasExitTime:是否有退出时间。简单理解:开启表示等待当前动画进行完才可进行下一个动画;关闭表示可以立即打断当前动画并播放下一个动画

    所以只有取消勾选才能在持续按前进的时候马上切换到Run动画

  • 巡逻兵状态机
    与玩家状态机类似,但是巡逻兵没有死亡动画,只有在于玩家碰撞时的射击动画。

预制制作

预制的制作有两个重点,一个是巡逻兵的碰撞检测,一个是地图的区域检测。

  • 巡逻兵碰撞检测:
    巡逻兵的碰撞检测分为两部分

    • 巡逻区域检测
      加入一个盒式碰撞体,并为其载入碰撞逻辑:
    巡逻区域对应是比较大的,所以如图所示设置的差不多即可,相关的碰撞逻辑为:
    public class PatrolZoneCollider : 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;
        }
      }
    }
    

    也就是当玩家进入巡逻区域时,会马上去追踪玩家。

    • 身体碰撞检测
      加入一个胶囊碰撞体,并为其载入碰撞逻辑:
    碰撞逻辑为:
    public class PatrolCollider : 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();
        }
      }
    }
    

    在玩家与巡逻兵直接碰撞后,玩家播放死亡动画、巡逻兵播放射击动画,并且游戏结束。

  • 地图内区域检测
    这个检测是为了实现最开始动图的效果,防止巡逻兵跨区域追踪。所以需要场景控制器维护一个关于玩家处于哪个区域的标价变量,并在玩家进入各个区域的碰撞体时触发区域标记更新。

    相关的检测逻辑很简单,只要修改玩家的所在区域标记即可:
    public class AreaCollider : MonoBehaviour {
          
          
      public int sign = 0;
      void OnTriggerEnter(Collider collider) {
          
          
        //标记玩家进入自己的区域
        if (collider.gameObject.tag == "Player") {
          
          
          FirstController firstController = SSDirector.GetInstance().CurrentScenceController as FirstController;
          firstController.wall_sign = sign;
        }
      }
    }
    
关键细节
  • 动作管理
    游戏要求我们巡逻兵会在玩家进入其巡逻区域时追踪,离开区域时继续其普通巡逻动作。那么根据之前动作管理者的职责,巡逻、追踪这两个小动作,在完成时应该通知动作管理者,于是,在通知的时候我们就可以来判断此时玩家的状态,从而使得决定下一个动作应该是追踪还是巡逻。
    于是动作管理者SSActionManager将要判断事件类型:

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

    而在两个动作简单类中,可以根据当前动作进行的状态判断未来动作应该是什么,举个例子就是说,如果当前巡逻兵正在追踪同一个区域内的玩家,如果被玩家逃离了,那么它应该通知动作管理者下一个动作是巡逻,于是向动作管理者发送的intParam参数为1.
    相对应的代码为:

    // PatrolFollowAction
    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);
      }
    
      transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime);
      this.transform.LookAt(player.transform.position);
    
      //如果侦察兵没有跟随对象,或者需要跟随的玩家不在侦查兵的区域内
      if (!data.follow_player || data.wall_sign != data.sign) {
          
          
        this.destroy = true;
        this.callback.SSActionEvent(this, 1, this.gameobject);
      }
    }
    
  • 订阅者模式

  • 事件发布者:专门发布事件的类,订阅者可以订阅该类的事件,通知者可以用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;
      // ...
    }
    
  • 订阅者:
    订阅者就是主场景控制器。订阅的方法如下,只要事件发布者检测到有人通知,就会调用这里注册的方法。

    void OnEnable() {
          
          
      GameEventManager.ScoreChange += AddScore;
      GameEventManager.GameoverChange += Gameover;
      GameEventManager.CrystalChange += ReduceCrystalNumber;
    }
    void OnDisable() {
          
          
      GameEventManager.ScoreChange -= AddScore;
      GameEventManager.GameoverChange -= Gameover;
      GameEventManager.CrystalChange -= ReduceCrystalNumber;
    }
    
  • 通知者:
    前面其实已经提到过,通知者就是每个碰撞检测代码,比如上面提到的巡逻兵身体碰撞检测:

    public class PatrolCollider : 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();
        }
      }
    }
    
  • 相机跟随
    为了视野更加广,需要将相机跟随人物,这样就不必显示和当前玩家位置太远的区域。

    public class CameraTrans : MonoBehaviour {
          
          
      public GameObject follow;            //跟随的物体
      public float smothing = 5f;          //相机跟随的速度
      public bool getInit = false;         //表明是否正确初始化了跟随物体
      Vector3 offset;                      //相机与物体相对偏移位置
    
      void FixedUpdate() {
          
          
        // 等待正确获得跟随物体
        if (!getInit) {
          
          
          if(follow == null) {
          
          
            return;
          }
          offset = transform.position - follow.transform.position;
          getInit = true;
        }
        //Vector3(0, 9, 0);主要是用于调整摄像机的高度
        Vector3 target = follow.transform.position + offset + new Vector3(0, 9, 0);
        //摄像机自身位置到目标位置平滑过渡
        transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime);
      }
    }
    
代码结构
  • 文件结构:
    Model Controller View Action
  • 类图:
    在这里插入图片描述
传送门

本次作业也是进行的很坎坷,主要是对于动画状态机的不熟悉造成的,另外感谢前文提到的优秀博客,通过模仿学习到了很多,并且也改进了一些代码,如果想要知道代码细节请前往gitee仓库

猜你喜欢

转载自blog.csdn.net/guokaijietti/article/details/109744440