Unity学习记录——模型与动画

2023-11-17

Unity学习记录——模型与动画

前言

​ 本文是中山大学软件工程学院2020级3d游戏编程与设计的作业7

编程题:智能巡逻兵

1.学习参考

​ 除去老师在课堂上讲的内容,本次作业代码与操作主要参考了[傅老師/Unity教學] DarkSouls複刻經典教程#第一季_bilibili

​ 模型动作资源也同样来自傅网课之中老师的给出教程人物模型与动画 (weiyun.com)

2.基本介绍

(1)unity动画1

​ Unity 有一个丰富而复杂的动画系统(有时称为“Mecanim”)。该系统具有以下功能:

  • 为 Unity 的所有元素(包括对象、角色和属性)提供简单工作流程和动画设置。
  • 支持导入的动画剪辑以及 Unity 内创建的动画
  • 人形动画重定向 - 能够将动画从一个角色模型应用到另一角色模型。
  • 对齐动画剪辑的简化工作流程。
  • 方便预览动画剪辑以及它们之间的过渡和交互。因此,动画师与工程师之间的工作更加独立,使动画师能够在挂入游戏代码之前为动画构建原型并进行预览。
  • 提供可视化编程工具来管理动画之间的复杂交互。
  • 以不同逻辑对不同身体部位进行动画化。
  • 分层和遮罩功能
(2)发布/订阅模式2

发布/订阅模式(Publish Subscribe Pattern)属于设计模式中的行为(Behavioral Patterns)。

​ 在软件架构中,发布/订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者),而是通过消息通道广播出去,让订阅改消息主题的订阅者消费到。

发布/订阅者模式最大的特点就是实现了松耦合,也就是说你可以让发布者发布消息、订阅者接受消息,而不是寻找一种方式把两个分离的系统连接在一起。当然这种松耦合也是发布/订阅者模式最大的缺点,因为需要中间的代理,增加了系统的复杂度。而且发布者无法实时知道发布的消息是否被每个订阅者接收到了,增加了系统的不确定性。

3.题目要求

智能巡逻兵

  • 提交要求:

  • 游戏设计要求:

    • 创建一个地图和若千巡逻兵(使用动画);

    • 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;

    • 巡逻兵碰撞到障碍物,则会自动选下一个点为目标

    • 巡逻兵在设定范围内感知到玩家,会自动追击玩家;

    • 失去玩家目标后,继续巡逻;

    • 计分: 玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;

  • 程序设计要求:

    • 必须使用订阅与发布模式传消息
      • subject: OnLostGoal
      • Publisher: ?
      • Subscriber: ?
      • 工厂模式生产巡逻兵
  • 友善提示1: 生成 3~5个边的凸多边型

    • 随机生成矩形
    • 在矩形每个边上随机找点,可得到 3 -4 的多边型
    • 5?
  • 友善提示2: 参考以前博客,给出自己新玩法

4.操作与代码详解

(一)Animator Controller制作
1)导入动画

​ 本次人物动画主要用到以下几个动作:

​ 分别为:待机,后跳,下落,跳跃,翻滚,奔跑,行走

请添加图片描述

2)绘制动画状态图

​ 按照如下变化绘制状态图

请添加图片描述

3)设置动画混合树

​ 动画混合树是动画状态机中的一个状态,也是多个动画的混合,可以实现多个类似动画的平滑过渡。

​ 右键ground点击Create new BlendTree in State创建动画混合树

请添加图片描述

​ 设置BlendTree,修改部分数值,并添加处于地面上的三个动画:待机、行走、奔跑

请添加图片描述

4)变迁

​ 添加转移控制变量如下:

请添加图片描述

​ 同时为各个变迁添加相应的控制条件变量,以地面状态到翻滚状态的变迁为例,如下:

请添加图片描述

5)FSM代码

​ 动画状态机之中,对应的动作会引起状态机状态的改变,同时也会带来对应的变化。如果直接在状态之中挂载代码,会使得项目冗杂,难以调试。此时可以使用发布/订阅模式,从状态机的变迁之中获得相应信息进行广播,

​ 编写了FSMClearSignals(),FSMOnEnter(),FSMOnExit(),FSMOnUpdate()四个函数用于消息的广播与状态机的初始化

​ 以下为FSMClearSignals()的代码,进行初始化,如下:

public class FSMClearSignals : StateMachineBehaviour {
    public string[] ClearAtEnter;
    public string[] ClearAtExit;

    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        foreach (var signal in ClearAtEnter) {
            animator.ResetTrigger(signal);
        }
    }

    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        foreach (var signal in ClearAtExit) {
            animator.ResetTrigger(signal);
        }
    }
}

​ 以下为进入状态的代码FSMOnEnter(),在进入状态时进行广播,FSMOnExit(),FSMOnUpdate()与此代码基本一致,此处不再放出

public class FSMOnEnter : StateMachineBehaviour {
    public string[] onEnterMessages;

    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        foreach (var msg in onEnterMessages) {
            animator.gameObject.SendMessageUpwards(msg);
        }
    }
}

​ 将代码挂载在对应状态下,此处仅以ground举例:

请添加图片描述

6)备注

​ 下载模型动画之后,为了适配本次作业,需要对一部分动作的设置进行修改,主要为Bake Into Pose选项的勾选,部分动作播放时object不需要产生旋转或位移,根据实际情况调整上面提到的七个动作即可

请添加图片描述

(二)Prefabs制作

再次声明:

此处涉及到玩家控制输入、落地检测、第三人称实现等较为复杂的代码与操作,基本都来自于[傅老師/Unity教學] DarkSouls複刻經典教程#第一季_bilibili

1)Player

​ 按照以下层级关系制作Player对象,其中仅ybot为模型,其余均为Empty

请添加图片描述

第三人称实现

​ 编写CameraConrtoller()控制摄像机,代码如下:

public class CameraConrtoller : MonoBehaviour {
    public PlayerInput pi;
    public float horizontalSpeed = 100f;
    public float verticalSpeed = 80f;
    public float cameraDampValue = 0.5f;

    private GameObject playerHandle;
    private GameObject cameraHandle;
    private float tempEulerX;
    private GameObject model;
    protected GameObject mycamera;

    private Vector3 cameraDampVelocity;
    

    void Awake() {
        cameraHandle = transform.parent.gameObject;
        playerHandle = cameraHandle.transform.parent.gameObject;
        model = playerHandle.GetComponent<ActorController>().model;
        mycamera = Camera.main.gameObject;
        tempEulerX = 20f;
    }

    // Update is called once per frame
    void FixedUpdate() {
        Vector3 tempModelEuler = model.transform.eulerAngles;
        playerHandle.transform.Rotate(Vector3.up, pi.Jright * horizontalSpeed * Time.fixedDeltaTime);
        tempEulerX -= pi.Jup * verticalSpeed * Time.fixedDeltaTime;
        tempEulerX = Mathf.Clamp(tempEulerX, -35, 30);
        cameraHandle.transform.localEulerAngles = new Vector3(tempEulerX, 0, 0);
        model.transform.eulerAngles = tempModelEuler;

        mycamera.transform.position = Vector3.SmoothDamp(
            mycamera.transform.position, transform.position, 
            ref cameraDampVelocity, cameraDampValue);
        mycamera.transform.eulerAngles = transform.eulerAngles;
    }
}

​ 将这一代码挂载在CameraPos上,Pi设置为Player

请添加图片描述

落地监测

​ 编写OnGroundSensor()用于落地检测,逻辑为添加一个与人物大小相似的CapsuleCollider(胶囊碰撞体),检测该碰撞体与地面层是否发生碰撞,代码如下:

public class OnGroundSensor : MonoBehaviour
{
    public CapsuleCollider capcol;
    public float offset = 0.1f;
    private Vector3 point1;
    private Vector3 point2;
    private float radius;
    void Awake()
    {
        radius = capcol.radius;
    }
    void FixedUpdate()
    {
        point1 = transform.position + transform.up * (radius - offset);
        point2 = transform.position + transform.up * (capcol.height - offset) - transform.up * radius;
        Collider[] outputCols = Physics.OverlapCapsule(point1, point2, radius, LayerMask.GetMask("Ground"));
        if (outputCols.Length != 0)
        {
            SendMessageUpwards("OnGround");
        }
        else
        {
            SendMessageUpwards("NotOnGround");
        }
    }
}

​ 该代码挂载在sensor上,capcol同样设置为Player

请添加图片描述

玩家控制模块

​ 代码如下:

public class PlayerInput : MonoBehaviour
{
    [Header("---- KeyCode Settings ----")]
    public string keyUp = "w";
    public string keyDown = "s";
    public string keyLeft = "a";
    public string keyRight = "d";

    public string keyA = "left shift";
    public string keyB = "space";
    public string keyC = "k";
    public string keyD;

    public string keyJUp = "up";
    public string keyJDown = "down";
    public string keyJLeft = "left";
    public string keyJRight = "right";

    [Header("---- Output Settings ----")]
    public float Dup;
    public float Dright;
    public float Dmag;//方向
    public Vector3 Dvec;//速度

    public float Jup;
    public float Jright;

    //1.pressing signal
    public bool run;
    public bool jump;
    private bool lastJump;
    //2.trigger signal
    //3.double signal

    [Header("---- Other Settings ----")]
    public bool inputEnabled = true;

    private float targetDup;
    private float targetDright;
    private float velocityDup;
    private float velocityDright;

    void Start() { }

    void Update()
    {
        Jup = (Input.GetKey(keyJUp)) ? 1.0f : 0 - (Input.GetKey(keyJDown) ? 1.0f : 0);
        Jright = (Input.GetKey(keyJRight)) ? 1.0f : 0 - (Input.GetKey(keyJLeft) ? 1.0f : 0);

        targetDup = (Input.GetKey(keyUp) ? 1.0f : 0) - (Input.GetKey(keyDown) ? 1.0f : 0);
        targetDright = (Input.GetKey(keyRight) ? 1.0f : 0) - (Input.GetKey(keyLeft) ? 1.0f : 0);

        if (!inputEnabled)
        {
            targetDup = 0;
            targetDright = 0;
        }
        //平滑变动
        Dup = Mathf.SmoothDamp(Dup, targetDup, ref velocityDup, 0.1f);
        Dright = Mathf.SmoothDamp(Dright, targetDright, ref velocityDright, 0.1f);

        /*矩形坐标转圆坐标*/
        Vector2 tempDAxis = SquareToCircle(new Vector2(Dup, Dright));
        float Dup2 = tempDAxis.x;
        float Dright2 = tempDAxis.y;

        Dmag = Mathf.Sqrt((Dup2 * Dup2) + (Dright2 * Dright2));
        Dvec = Dright * transform.right + Dup * transform.forward;
        run = Input.GetKey(keyA);

        /*跳跃*/
        bool newJump = Input.GetKey(keyB);
        lastJump = jump;
        if (lastJump == false && newJump == true)
        {
            jump = true;
        }
        else
        {
            jump = false;
        }
    }

    /*矩形坐标转圆坐标*/
    private Vector2 SquareToCircle(Vector2 input)
    {
        Vector2 output = Vector2.zero;
        output.x = input.x * Mathf.Sqrt(1 - (input.y * input.y) / 2.0f);
        output.y = input.y * Mathf.Sqrt(1 - (input.x * input.x) / 2.0f);
        return output;
    }
}
角色控制模块

​ 代码如下:

public class ActorController : MonoBehaviour
{
    public GameObject model;
    public PlayerInput pi;
    public float walkSpeed = 1.5f;
    public float runMultiplier = 2.7f;
    public float jumpVelocity = 4f;
    public float rollVelocity = 1f;

    [SerializeField]
    private Animator anim;
    private Rigidbody rigid;
    private Vector3 planarVec; // 平面移动向量
    private Vector3 thrustVec; // 跳跃冲量

    private bool lockPlanar = false; // 跳跃时锁死平面移动向量

    void Awake()
    {
        pi = GetComponent<PlayerInput>();
        anim = model.GetComponent<Animator>();
        rigid = GetComponent<Rigidbody>();
    }

    //刷新每秒60次
    void Update()
    {
        //修改动画混合树
        /*1.从走路到跑步没有过渡*/
        /*anim.SetFloat("forward", pi.Dmag * (pi.run ? 2.0f : 1.0f));*/
        /*2.使用Lerp加权平均解决*/
        float targetRunMulti = pi.run ? 2.0f : 1.0f;
        anim.SetFloat("forward", pi.Dmag * Mathf.Lerp(anim.GetFloat("forward"), targetRunMulti, 0.3f));
        //播放翻滚动画
        if (rigid.velocity.magnitude > 1.0f)
        {
            anim.SetTrigger("roll");
        }
        //播放跳跃动画
        if (pi.jump)
        {
            anim.SetTrigger("jump");
        }
        //转向
        if (pi.Dmag > 0.01f)
        {
            /*1.旋转太快没有补帧*/
            /*model.transform.forward = pi.Dvec;*/
            /*2.使用Slerp内插值解决*/
            Vector3 targetForward = Vector3.Slerp(model.transform.forward, pi.Dvec, 0.2f);
            model.transform.forward = targetForward;
        }
        if (!lockPlanar)
        {
            //保存供物理引擎使用
            planarVec = pi.Dmag * model.transform.forward * walkSpeed * (pi.run ? runMultiplier : 1.0f);
        }

    }

    //物理引擎每秒50次
    private void FixedUpdate()
    {
        //Time.fixedDeltaTime 50/s
        //1.修改位置
        //rigid.position += movingVec * Time.fixedDeltaTime;
        //2.修改速度
        rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z) + thrustVec;
        //一帧
        thrustVec = Vector3.zero;
    }

    public void OnJumpEnter()
    {
        pi.inputEnabled = false;
        lockPlanar = true;
        thrustVec = new Vector3(0, jumpVelocity, 0);
    }

    public void OnRollEnter()
    {
        pi.inputEnabled = false;
        lockPlanar = true;
    }

    public void OnRollUpdate()
    {
        thrustVec = model.transform.forward * anim.GetFloat("rollVelocity") * 1.0f;
    }

    public void OnGround()
    {
        anim.SetBool("OnGround", true);
    }

    public void NotOnGround()
    {
        anim.SetBool("OnGround", false);
    }

    public void OnGroundEnter()
    {
        pi.inputEnabled = true;
        lockPlanar = false;
    }

    public void OnFallEnter()
    {
        pi.inputEnabled = false;
        lockPlanar = true;
    }

    public void OnJabEnter()
    {
        pi.inputEnabled = false;
        lockPlanar = true;
    }

    public void OnJabUpdate()
    {
        thrustVec = model.transform.forward * anim.GetFloat("jabVelocity") * 1.4f;
    }
}
碰撞事件实现

​ 代码如下:

public class PlayerCollide : MonoBehaviour {
    void OnCollisionEnter(Collision other) {
        //当玩家与侦察兵相撞
        if (other.gameObject.tag == "Guard") {
            Singleton<GameEventManager>.Instance.PlayerGameover();
        }
    }
}
挂载与组件相关

​ 上述三处代码均挂载在Player之上,同时需要给Player添加刚体组件与胶囊碰撞体,如下:

请添加图片描述

​ 同时需要给ybot的Animator组件添加之前制作的Animator Controller,如下:

请添加图片描述

2)Guard

​ 按照以下层级关系制作Guard对象,其中仅ybot为模型,Guard父对象为为Empty

请添加图片描述

巡逻兵数据模块

​ 代码如下,主要为给巡逻兵实体设置后续所需的数据:

public class GuardData : MonoBehaviour {
    public GameObject model;
    public float walkSpeed = 1.2f;
    public float runSpeed = 2.5f;
    public int sign;                      //标志巡逻兵在哪一块区域
    public bool isFollow = false;         //是否跟随玩家
    public int playerSign = -1;           //当前玩家所在区域标志
    public Vector3 start_position;        //当前巡逻兵初始位置   

    [SerializeField]
    private Animator anim;
    private Rigidbody rigid;

    void Awake() {
        anim = model.GetComponent<Animator>();
        rigid = GetComponent<Rigidbody>();
    }

    public void OnGround() {
        anim.SetBool("OnGround", true);
    }
    public void OnGroundEnter() {
        
    }
}
挂载与组件相关

​ 上述代码需要挂载在Guard之上,同时需要给Guard添加刚体组件与胶囊碰撞体,如下:

请添加图片描述

​ 注:此处ybot的Animator组件无需再添加之前制作的Animator Controller

3)Plane

​ Plane为本次项目中玩家与巡逻兵行动的平台,预制如下:

​ 其中Plane父对象,Sensor,Trigger均为Empty,Plane子对象为地面,Wall为不同的前面

请添加图片描述

请添加图片描述

碰撞事件实现

​ 在Player与地面或墙面碰撞之后,传出Player所处区域,代码如下:

public class AreaCollide : MonoBehaviour {
    public int sign = 0;
    private FirstSceneController sceneController;

    private void Start() {
        sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
    }

    void OnTriggerEnter(Collider collider) {
        if (collider.gameObject.tag == "Player") {
            sceneController.playerSign = sign;
        }
    }
}
挂载与组件相关

​ 在建立Plane父对象时,需要给Plane设置Layer为Ground,才可以实现Player中的落地监测函数

请添加图片描述

​ 需要给各个Triger设置相应大小的碰撞体,挂载AreaCollide()函数

请添加图片描述

(三)代码实现
1)动作分离

​ 和之前作业中的动作分离基本一致,包括了SSAction()动作基类,SSActionManager()动作管理类,这里就不再放出了

​ 实现巡逻兵的动作管理类为GuardActionManager(),实现了巡逻与追逐玩家两个动作,代码如下:

public class GuardActionManager : SSActionManager, ISSActionCallback {
    private GuardPatrolAction patrol;
    private GameObject player;
    public void GuardPatrol(GameObject guard, GameObject _player) {
        player = _player;
        patrol = GuardPatrolAction.GetSSAction(guard.transform.position);
        this.RunAction(guard, patrol, this);
    }

    public void SSActionEvent(
        SSAction source, SSActionEventType events = SSActionEventType.Competeted,
        int intParam = 0, GameObject objectParam = null) {
        if (intParam == 0) {
            //追逐
            GuardFollowAction follow = GuardFollowAction.GetSSAction(player);
            this.RunAction(objectParam, follow, this);
        } else {
            //巡逻
            GuardPatrolAction move = GuardPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<GuardData>().start_position);
            this.RunAction(objectParam, move, this);
            Singleton<GameEventManager>.Instance.PlayerEscape();
        }
    }
}
2)巡逻兵工厂模式

​ 使用工厂模式批量生成巡逻兵,由于已经在鼠标打飞碟那一节课程学习过了,此处就不再介绍代码实现,仅放出代码如下:

public class GuardFactory : MonoBehaviour {
    private GameObject guard = null;                               //巡逻兵
    private List<GameObject> used = new List<GameObject>();        //正在使用的巡逻兵列表
    private Vector3[] vec = new Vector3[9];                        //每个巡逻兵的初始位置

    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 < 8; i++) {
            guard = Instantiate(Resources.Load<GameObject>("Prefabs/Guard"));
            guard.transform.position = vec[i];
            guard.GetComponent<GuardData>().sign = i + 1;
            guard.GetComponent<GuardData>().start_position = vec[i];
            guard.GetComponent<Animator>().SetFloat("forward", 1);
            used.Add(guard);
        }   
        return used;
    }
}
3)巡逻兵移动策略

​ 分成了两个部分,巡逻与追逐

​ 巡逻即在一个矩形范围内重复移动,巡逻代码如下:

public class GuardPatrolAction : SSAction {
    private enum Dirction { EAST, NORTH, WEST, SOUTH };
    private float pos_x, pos_z;                 
    private float move_length;                 
    private bool move_sign = true;              
    private Dirction dirction = Dirction.EAST;  

    private GuardData data;
    private Animator anim;
    private Rigidbody rigid;
    private Vector3 planarVec; // 平面移动向量
    private GuardPatrolAction() { }

    public override void Start() {
        data = gameobject.GetComponent<GuardData>();
        anim = gameobject.GetComponent<Animator>();
        rigid = gameobject.GetComponent<Rigidbody>();
        //播放走路动画
        anim.SetFloat("forward", 1.0f);
    }
    public static GuardPatrolAction GetSSAction(Vector3 location) {
        GuardPatrolAction action = CreateInstance<GuardPatrolAction>();
        action.pos_x = location.x;
        action.pos_z = location.z;
        //设定移动矩形的边长
        action.move_length = Random.Range(5, 6);
        return action;
    }

    public override void Update() {
        //保留供物理引擎调用
        planarVec = gameobject.transform.forward * data.walkSpeed;
    }

    public override void FixedUpdate() {
        //巡逻
        Gopatrol();
        //玩家进入该区域,巡逻结束,开始追逐
        if (data.playerSign == data.sign) {
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 0, this.gameobject);
        }
    }

    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) {
            rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z);
        } else {
            dirction = dirction + 1;
            if(dirction > Dirction.SOUTH) {
                dirction = Dirction.EAST;
            }
            move_sign = true;
        }
    }
}

​ 追逐发生在玩家进入巡逻区域之后,玩家进入之后会向着玩家方向前进,当玩家离开巡逻范围则进行巡逻

public class GuardFollowAction : SSAction {
    private GameObject player;        
    private GuardData data;
    private Animator anim;
    private Rigidbody rigid;
    private Vector3 planarVec; // 平面移动向量
    private float speed;

    private GuardFollowAction() {}

    public override void Start() {
        data = gameobject.GetComponent<GuardData>();
        anim = gameobject.GetComponent<Animator>();
        rigid = gameobject.GetComponent<Rigidbody>();
        speed = data.runSpeed;
        anim.SetFloat("forward", 2.0f);
    }

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

    public override void Update() {
        //保留供物理引擎调用
        planarVec = gameobject.transform.forward * speed;
    }

    public override void FixedUpdate() {
        transform.LookAt(player.transform.position);
        rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z);
        
        //如果玩家脱离该区域则继续巡逻
        if (data.playerSign != data.sign) {
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 1, this.gameobject);
        }
    }
}
4)发布/订阅模式

​ 编写了GameEventManager()进行全局性的发布消息,接受者部分消息方则实现在FirstSceneController()

public class GameEventManager : MonoBehaviour {
    public delegate void ScoreEvent();
    public static event ScoreEvent ScoreChange;
    
    public delegate void GameoverEvent();
    public static event GameoverEvent GameoverChange;

    public void PlayerEscape() {
        if (ScoreChange != null) {
            ScoreChange();
        }
    }

    public void PlayerGameover(){
        if (GameoverChange != null) {
            GameoverChange();
        }
    }
}
5)其他代码

​ 项目其他代码如:SSDirector(),Singleton(),UserGUI(),Interface()等就不再放出,请移步文章最后到项目之中查看

演示

​ 本次由于录制视频较大,就不再制作gif图。演示视频已经投稿至b站智能巡逻兵-哔哩哔哩-bilibili

代码位置

​ 代码以及文档均已经上传至hw7 · XiaoChen04_3/3D_Computer_Game_Programming - gitee


  1. 动画系统概述 - Unity 手册 ↩︎

  2. 设计模式之发布订阅模式(1) 一文搞懂发布订阅模式 - 腾讯云开发者社区-腾讯云 (tencent.com) ↩︎

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Unity学习记录——模型与动画 的相关文章

随机推荐

  • 【华为OD统一考试A卷

    华为OD统一考试A卷 B卷 新题库说明 2023年5月份 华为官方已经将的 2022 0223Q 1 2 3 4 统一修改为OD统一考试 A卷 和OD统一考试 B卷 你收到的链接上面会标注A卷还是B卷 请注意 根据反馈 目前大部分收到的都是
  • Kali系统(Debian 10.3) 遇到的问题

    目录 问题一 Kali系统 相关技术网站 博客 文章 论坛 工具包 包跟踪 提交BUG 问题二 黑客入门 手痒地方 问题三 Kali系统 MySQL问题Can t connect to local MySQL server through
  • 边缘计算操作系统安装及测试实验报告

    边缘计算操作系统安装及测试 一 实验目的 二 实验环境 三 实验原理 1 系统组成部分 2 总体数据流程 四 实验步骤及结果 1 安装 Docker 和 Docker Compose 2 下载 EdgeX compose 文件 3 运行Ed
  • qt中clicked(bool checked)和toggled(bool checked)的区别

    先来看qt文档的解释 上面看出 共同点是 当点击按钮时 状态信号都会被发送 不同点 clicked this signal is not emitted if you call setDown setChecked or toggle to
  • 5年测试面试要20K,面试三个问题把我打发走了···

    都说金三银四 金九银十跳槽涨薪季 我是着急忙慌的准备简历 5年软件测试经验 可独立测试大型产品项目 熟悉项目测试流程 薪资要求 5年测试经验起码能要个20K吧 我加班肝了一页半简历 投出去一周 面试电话倒是不少 自信满满去面试 现场被问了这
  • Nmap源码分析(服务与版本扫描)

    Nmap源码分析 服务与版本扫描 2012年8月23日 在进行端口扫描后 Nmap可以进一步探测出运行在端口上的服务类型及应用程序的版本 目前Nmap可以识别几千种服务程序的签名 Signature 覆盖了180多种应用协议 比如 端口扫描
  • java写后端接口中mapper的一些操作

    文章目录 Mybatis Mapper的动态SQL语句问题 一 if 二 choose when otherwise 三 where 四 trim 元素来定制 where 元素的功能 五 set 动态地在行首插入 SET 关键字 六 for
  • PTA 7-4 统计学生平均成绩与及格人数 (15 分)

    本题要求编写程序 计算学生们的平均成绩 并统计及格 成绩不低于60分 的人数 题目保证输入与输出均在整型范围内 输入格式 输入在第一行中给出非负整数N 即学生人数 第二行给出N个非负整数 即这N位学生的成绩 其间以空格分隔 输出格式 按照以
  • C语言函数大全-- y 开头的函数

    y 开头的函数 1 yperror 1 1 函数说明 1 2 演示示例 2 yp match 2 1 函数说明 2 2 演示示例 3 y0 零阶第二类贝塞尔函数 3 1 函数说明 3 2 演示示例 3 3 运行结果 4 y1 一阶第二类贝塞
  • 在Vue中使用flex布局 echarts多图标不能自适应缩放问题

    前言 最近有个项目需要用到echarts绘制多个图表 需求是要支持大屏展示 还有需要支持不同比例的缩放和任意手动缩放 因此 深入学习了echarts和flex布局 虽然遇到很多问题 但都一一解决了收获良多 故此写下遇到的问题与坑 与之共勉
  • go 进阶 多路复用支持: 二. Accept/Read/Write

    目录 一 通过httpServer服务端引用Accept 二 Listener Accept 等待连接 三 Conn Read读数据 Conn Write写数据 四 gopark 阻塞 五 netpoll 唤醒等待队列中挂起的协程 什么时候
  • C#桌面应用程序打包

    使用微软的技术开发windows桌面应用程序是很快捷方便的 开发完之后肯定要打包安装才能发布 以前有做过但过长时间没有打包一下子还真有些遗忘 今天专门又重温了一些 干脆写下来算是加深些印象 以后需要时也好有个参考 感觉有很多技术上手都没有太
  • std::bind可以绑定成员变量

    include
  • java student类_java定义一个Student类,包含内容如下

    展开全部 public class Student 成员变量 学号 姓名 性别 班干部否 数学 语文 外语 成62616964757a686964616fe58685e5aeb931333337613166员方法 输入 总分 平均分 编程实
  • MeterSphere实践指南汇总,搬砖党

    闲来无事 整理了MeterSphere实践指南 我司用了MeterSphere一段时间 感觉挺好用 百度网盘下载链接 链接 https pan baidu com s 1s8sAuz31lgnvTRTLkWZuiQ pwd 98bg 提取码
  • 我的算法笔记(1)——冒泡排序

    我的算法笔记 1 冒泡排序 排序是指将一个无序序列按某个规则进行有序排列 而冒泡排序是排序算法中最基础的一种 现给出一个序列a 其中元素的个数为n 要求将他们按从小到大的顺序排序 冒泡排序的本质在于交换 即每次通过交换的方式把当前剩余元素的
  • BP神经网络阈值

    在基于神经网络的数据融合文章里 有改进权值和阈值 但是没有说清阈值到底是什么 神经网络是模仿大脑的神经元 当外界刺激达到一定的阈值时 神经元才会受到刺激 影响下一个神经元 简单来说 超过阈值 就会引起某一变化 不超过阈值 无论是多少 都不产
  • 【数据库实验】sql总结

    首先说明 以下大部分针对的是标准sql 目录 结构 关键词 关于模式 创建模式 删除模式 关于表 创建表 修改表 删除表 关于索引 建立索引 修改索引 删除索引 关于查询 几个点 指定列 全部列 经过计算的值 列的别名 方便查看 以及聚集函
  • 深度学习:将新闻报道按照不同话题性质进行分类

    深度学习的广泛运用之一就是对文本按照其内容进行分类 例如对新闻报道根据其性质进行划分是常见的应用领域 在本节 我们要把路透社自1986年以来的新闻数据按照46个不同话题进行划分 网络经过训练后 它能够分析一篇新闻稿 然后按照其报道内容 将其
  • Unity学习记录——模型与动画

    Unity学习记录 模型与动画 前言 本文是中山大学软件工程学院2020级3d游戏编程与设计的作业7 编程题 智能巡逻兵 1 学习参考 除去老师在课堂上讲的内容 本次作业代码与操作主要参考了 傅老師 Unity教學 DarkSouls複刻經